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 14:24:18 UTC

(causeway) branch htmx updated (65177d18825 -> 1dc8d3ef3d4)

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 65177d18825 docs(openspec): archive full-width sample pages
     new 61e126c61fd docs(openspec): propose collection headings
     new da560ae6741 feat(webcomponents): enhance collection headings
     new 88203c35bf6 docs(openspec): archive collection headings
     new 73a220a1467 docs(openspec): propose narrow multiline layout fix
     new c8384ec734d fix(webcomponents): align narrow multiline properties
     new 1dc8d3ef3d4 docs(openspec): archive narrow multiline layout fix

The 6 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                                      | 67 ++++++++++++++++++
 .../proposal.md                                    | 32 +++++++++
 .../specs/domain-web-components/spec.md            | 42 +++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 27 ++++++++
 .../specs/graphql-web-component-context/spec.md    | 19 +++++
 .../tasks.md                                       | 24 +++++++
 .../.openspec.yaml                                 |  0
 .../design.md                                      | 63 +++++++++++++++++
 .../proposal.md                                    | 27 ++++++++
 .../specs/domain-web-components/spec.md            | 35 ++++++++++
 .../tasks.md                                       | 16 +++++
 openspec/specs/domain-web-components/spec.md       | 49 ++++++++++++-
 .../generic-htmx-web-component-viewer/spec.md      | 26 +++++++
 .../specs/graphql-web-component-context/spec.md    | 18 +++++
 viewers/webcomponents/foundation/USAGE.adoc        | 12 +++-
 .../foundation/src/collection-element.mjs          | 54 ++++++++++-----
 .../foundation/src/object-context-controller.mjs   |  8 ++-
 .../foundation/src/property-element.mjs            | 13 ++--
 viewers/webcomponents/foundation/src/theme.css     | 48 ++++++-------
 .../foundation/test/collection.test.mjs            | 81 ++++++++++++++++++++++
 .../foundation/test/component-styles.test.mjs      | 19 +++--
 .../foundation/test/components.test.mjs            | 37 ++++++++++
 .../test/fixtures/rich-schema-fixture.mjs          | 14 +++-
 .../foundation/test/interaction.test.mjs           |  2 +
 .../foundation/test/member-composition.test.mjs    |  4 +-
 .../foundation/test/object-context.test.mjs        | 50 +++++++++++++
 .../htmx/petclinic/domain/PetClinicHomePage.java   |  4 +-
 .../sample/htmx/petclinic/domain/PetOwner.java     |  4 +-
 .../webcomponents/pages/petclinic.HomePage.html    |  2 +-
 .../webcomponents/pages/petclinic.PetOwner.html    |  6 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 27 +++++++-
 .../PetClinicHtmxApplication_IntegTest.java        | 23 ++++--
 33 files changed, 780 insertions(+), 73 deletions(-)
 copy openspec/changes/archive/{2026-08-29-add-breadcrumbs-web-component => 2026-08-29-enhance-collection-headings}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/design.md
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/proposal.md
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/domain-web-components/spec.md
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/graphql-web-component-context/spec.md
 create mode 100644 openspec/changes/archive/2026-08-29-enhance-collection-headings/tasks.md
 copy openspec/changes/archive/{2026-08-29-add-breadcrumbs-web-component => 2026-08-29-fix-narrow-multiline-property-layout}/.openspec.yaml (100%)
 create mode 100644 openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/design.md
 create mode 100644 openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/proposal.md
 create mode 100644 openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
 create mode 100644 openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/tasks.md


(causeway) 04/06: docs(openspec): propose narrow multiline layout fix

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 73a220a1467cebfa606de61fcb3dfdac2c0be5ca
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 16:04:54 2026 +0200

    docs(openspec): propose narrow multiline layout fix
---
 .../.openspec.yaml                                 |  2 +
 .../fix-narrow-multiline-property-layout/design.md | 61 ++++++++++++++++++++++
 .../proposal.md                                    | 27 ++++++++++
 .../specs/domain-web-components/spec.md            | 35 +++++++++++++
 .../fix-narrow-multiline-property-layout/tasks.md  | 16 ++++++
 5 files changed, 141 insertions(+)

diff --git a/openspec/changes/fix-narrow-multiline-property-layout/.openspec.yaml b/openspec/changes/fix-narrow-multiline-property-layout/.openspec.yaml
new file mode 100644
index 00000000000..50adc91034f
--- /dev/null
+++ b/openspec/changes/fix-narrow-multiline-property-layout/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-29
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/design.md b/openspec/changes/fix-narrow-multiline-property-layout/design.md
new file mode 100644
index 00000000000..15fa3d7074f
--- /dev/null
+++ b/openspec/changes/fix-narrow-multiline-property-layout/design.md
@@ -0,0 +1,61 @@
+## Context
+
+`CausewayPropertyElement` correctly resolves multiline depth from canonical `multi-line`, legacy `multiline`, or GraphQL `metadata.multiLine`.
+Its editor receives that effective value, but the theme's layout selectors inspect only `cw-property[multiline]`.
+For a canonical Petclinic `multi-line="5"` property, the multiline row rules therefore do not match.
+At narrow width, generic grid declarations move the value but leave the edit control auto-placed, producing an implicit-grid offset of roughly 2.4 pixels and failing the explicit alignment assertion.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make property layout consume the same effective multiline state as rendering and editing.
+- Give every multiline shell state explicit wide and narrow grid placement.
+- Remove overlap and retain bounded controls and no document overflow.
+- Cover canonical, legacy, and metadata-derived multiline inputs.
+
+**Non-Goals:**
+
+- Relax the browser assertion to conceal incorrect grid placement.
+- Change multiline editor selection or row-count semantics.
+- Modify Vaadin components, generated toolkit assets, or application-local CSS.
+- Change property label-position behavior unrelated to multiline layout.
+
+## Decisions
+
+### Publish effective multiline state on the generated property shell
+
+Each rendered `.causeway-property` shell will carry `data-multi-line="N"` when the effective multiline value is positive.
+The shell is regenerated for loading, error, view, disabled, and edit states, so the hook remains synchronized with canonical HTML, compatibility HTML, and metadata.
+
+Selecting both host attribute spellings in CSS was rejected because it would still miss metadata-only multiline state.
+Reflecting metadata back onto application-authored `<cw-property>` was rejected because internal presentation should not mutate public source declarations.
+
+### Select the framework-owned shell in layout CSS
+
+Wide, container-narrow, and viewport-narrow multiline rules will target `.causeway-property[data-multi-line]` and explicitly place label, description, value, and edit control.
+This uses the element that is actually the grid container and avoids dependence on ancestor attribute spelling.
+
+Increasing geometry tolerance was rejected because the failure exposed auto-placement rather than harmless rounding.
+Adding a cosmetic edit-button margin was rejected because it would mask the missing grid-row contract and could misalign other states.
+
+### Assert effective state and geometry
+
+Foundation tests will verify shell attributes for canonical, compatibility, and metadata inputs and stylesheet selectors for all responsive rules.
+Petclinic Playwright will retain the strict ordering checks and add bounded relative-alignment diagnostics where useful.
+
+## Risks / Trade-offs
+
+- [Risk] A generated data attribute becomes stale after metadata or authored attributes change. → Derive it during every existing shell render from `#effectiveMultiLine(state)`.
+- [Risk] Selector replacement changes wide layout. → Preserve the same columns and rows and test wide and narrow paths.
+- [Risk] Error or loading shells omit the hook. → Centralize attribute generation and apply it to every shell template.
+
+## Migration Plan
+
+No application migration is required.
+Both `multi-line` and `multiline` remain accepted, and metadata-derived multiline behavior becomes consistent with them.
+Rollback restores the host selector and removes the generated shell hook.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/proposal.md b/openspec/changes/fix-narrow-multiline-property-layout/proposal.md
new file mode 100644
index 00000000000..018496bb574
--- /dev/null
+++ b/openspec/changes/fix-narrow-multiline-property-layout/proposal.md
@@ -0,0 +1,27 @@
+## Why
+
+Narrow native-toolkit rendering can misplace a multiline property's edit control because responsive CSS detects only the legacy `multiline` host attribute, while authored `multi-line` and metadata-derived multiline state do not activate the intended grid rows.
+The resulting implicit grid placement creates a small but real vertical overlap and leaves native Playwright acceptance failing.
+
+## What Changes
+
+- Expose effective multiline presentation as a framework-owned attribute on the rendered property shell in every component state.
+- Drive wide and narrow multiline layout selectors from that effective shell state rather than one host-attribute spelling.
+- Preserve support for canonical `multi-line`, legacy `multiline`, and metadata-derived `multiLine` inputs.
+- Strengthen component stylesheet and Petclinic browser coverage for narrow native alignment, no overlap, and no overflow.
+- Leave Vaadin toolkit internals and application-specific styling unchanged.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `domain-web-components`: Require responsive multiline property layout to follow effective presentation metadata and place labels, descriptions, values, editors, and controls in explicit non-overlapping grid rows.
+
+## Impact
+
+Affected code is limited to the framework property shell, shared semantic theme selectors, foundation tests, and existing Petclinic acceptance geometry.
+There are no GraphQL schema, domain model, route, persistence, dependency, or toolkit implementation changes.
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md b/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..2ee584a2e1d
--- /dev/null
+++ b/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
@@ -0,0 +1,35 @@
+## MODIFIED Requirements
+
+### Requirement: Property label-position presentation
+`<cw-property>` SHALL honour the effective property label position `LEFT`, `TOP`, or `NONE` in view, loading, error, disabled, and edit states.
+Properties using `LEFT` in the same field-set-like container MUST use a consistent configurable label-to-field ratio.
+Effective multiline presentation from canonical HTML, compatibility HTML, or member metadata MUST drive the same explicit responsive shell layout.
+
+#### Scenario: Label is positioned left
+- **WHEN** the effective label position is `LEFT`
+- **THEN** the visible label is placed to the left of the property field using the container's consistent label-column ratio
+- **AND** any description appears in smaller text below the label
+- **AND** the field uses the remaining width
+
+#### Scenario: Label is positioned top
+- **WHEN** the effective label position is `TOP`
+- **THEN** the visible label is placed above the property field
+- **AND** any description appears in smaller text below the label
+- **AND** the field uses the full available width
+
+#### Scenario: Label is suppressed
+- **WHEN** the effective label position is `NONE`
+- **THEN** no visible label or description is rendered
+- **AND** the field uses the full available width
+- **AND** interactive controls retain a meaningful accessible name
+
+#### Scenario: Property description comes from the facet
+- **WHEN** `@PropertyLayout(describedAs)` supplies a non-redundant description and no HTML override is present
+- **THEN** the description appears in smaller text immediately below the visible label
+- **AND** the value or editor is associated with it through accessible description semantics
+
+#### Scenario: Narrow presentation stacks safely
+- **WHEN** a multiline `LEFT` property resolved from `multi-line`, legacy `multiline`, or `metadata.multiLine` cannot retain its configured columns at a narrow inline size
+- **THEN** label, description, field, and controls occupy explicit successive grid rows in meaningful document order
+- **AND** the edit control aligns with its field without overlap or implicit-grid displacement
+- **AND** the property introduces no horizontal overflow or clipping
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/tasks.md b/openspec/changes/fix-narrow-multiline-property-layout/tasks.md
new file mode 100644
index 00000000000..2ef9a34ae05
--- /dev/null
+++ b/openspec/changes/fix-narrow-multiline-property-layout/tasks.md
@@ -0,0 +1,16 @@
+## 1. Effective multiline shell state
+
+- [ ] 1.1 Add a generated property-shell attribute derived from effective canonical, compatibility, or metadata multiline state in every render state.
+- [ ] 1.2 Replace host-spelling-dependent multiline selectors with shell-state selectors while preserving wide and narrow grid placement.
+
+## 2. Regression coverage
+
+- [ ] 2.1 Add property component tests for canonical, legacy, and metadata-derived shell state across view, loading, error, and edit rendering.
+- [ ] 2.2 Update stylesheet regression tests to require effective shell selectors and reject the incomplete legacy host selector.
+- [ ] 2.3 Strengthen Petclinic geometry diagnostics and verify native narrow multiline alignment, bounded controls, and no overflow.
+
+## 3. Validation and archive readiness
+
+- [ ] 3.1 Run foundation tests and package verification.
+- [ ] 3.2 Run focused and full Petclinic Playwright acceptance in default and native modes.
+- [ ] 3.3 Run Petclinic integration, strict OpenSpec validation, compilation, license, diff, and working-tree checks.


(causeway) 02/06: feat(webcomponents): enhance collection headings

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 da560ae67411ca10224b484643f9aa0c2b1554bf
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 15:53:26 2026 +0200

    feat(webcomponents): enhance collection headings
---
 .../changes/enhance-collection-headings/tasks.md   | 26 +++----
 viewers/webcomponents/foundation/USAGE.adoc        | 12 +++-
 .../foundation/src/collection-element.mjs          | 54 ++++++++++-----
 .../foundation/src/object-context-controller.mjs   |  8 ++-
 viewers/webcomponents/foundation/src/theme.css     | 10 ++-
 .../foundation/test/collection.test.mjs            | 81 ++++++++++++++++++++++
 .../foundation/test/component-styles.test.mjs      |  6 ++
 .../test/fixtures/rich-schema-fixture.mjs          | 14 +++-
 .../foundation/test/member-composition.test.mjs    |  4 +-
 .../foundation/test/object-context.test.mjs        | 50 +++++++++++++
 .../htmx/petclinic/domain/PetClinicHomePage.java   |  4 +-
 .../sample/htmx/petclinic/domain/PetOwner.java     |  4 +-
 .../webcomponents/pages/petclinic.HomePage.html    |  2 +-
 .../webcomponents/pages/petclinic.PetOwner.html    |  6 +-
 .../petclinic/PetClinicHtmxPlaywrightTest.java     | 23 ++++++
 .../PetClinicHtmxApplication_IntegTest.java        | 23 ++++--
 16 files changed, 280 insertions(+), 47 deletions(-)

diff --git a/openspec/changes/enhance-collection-headings/tasks.md b/openspec/changes/enhance-collection-headings/tasks.md
index 3f0bb0b01d8..cdc15b13075 100644
--- a/openspec/changes/enhance-collection-headings/tasks.md
+++ b/openspec/changes/enhance-collection-headings/tasks.md
@@ -1,24 +1,24 @@
 ## 1. Collection metadata and heading behavior
 
-- [ ] 1.1 Extend targeted collection requirements to select supported canonical friendly-name and description metadata.
-- [ ] 1.2 Implement `named` and `described-as` collection attributes with documented precedence, compatibility, duplicate suppression, and reactive rerendering.
-- [ ] 1.3 Remove visible collection-level disabled or unmodifiable reason markup while preserving read-only and member/action semantics.
-- [ ] 1.4 Refine collection description styling below the label with accessible subdued typography.
+- [x] 1.1 Extend targeted collection requirements to select supported canonical friendly-name and description metadata.
+- [x] 1.2 Implement `named` and `described-as` collection attributes with documented precedence, compatibility, duplicate suppression, and reactive rerendering.
+- [x] 1.3 Remove visible collection-level disabled or unmodifiable reason markup while preserving read-only and member/action semantics.
+- [x] 1.4 Refine collection description styling below the label with accessible subdued typography.
 
 ## 2. Foundation regression coverage
 
-- [ ] 2.1 Add object-context tests for targeted collection metadata selection, partial support, and request reuse.
-- [ ] 2.2 Add collection component tests for canonical and HTML heading precedence, reactive attributes, accessibility, duplicate suppression, and quiet unmodifiable state.
-- [ ] 2.3 Update stylesheet and component-contract coverage for the new public attributes and description presentation.
+- [x] 2.1 Add object-context tests for targeted collection metadata selection, partial support, and request reuse.
+- [x] 2.2 Add collection component tests for canonical and HTML heading precedence, reactive attributes, accessibility, duplicate suppression, and quiet unmodifiable state.
+- [x] 2.3 Update stylesheet and component-contract coverage for the new public attributes and description presentation.
 
 ## 3. Petclinic demonstrations
 
-- [ ] 3.1 Add selective `@CollectionLayout(describedAs)` descriptions to Petclinic collections.
-- [ ] 3.2 Add selective `named` and `described-as` overrides to Petclinic HTML pages while leaving other collections metadata-driven or unchanged.
-- [ ] 3.3 Extend Petclinic integration and Playwright assertions for names, descriptions, omitted unmodifiable noise, and unaffected interactions.
+- [x] 3.1 Add selective `@CollectionLayout(describedAs)` descriptions to Petclinic collections.
+- [x] 3.2 Add selective `named` and `described-as` overrides to Petclinic HTML pages while leaving other collections metadata-driven or unchanged.
+- [x] 3.3 Extend Petclinic integration and Playwright assertions for names, descriptions, omitted unmodifiable noise, and unaffected interactions.
 
 ## 4. Validation and archive readiness
 
-- [ ] 4.1 Run foundation tests and package verification.
-- [ ] 4.2 Run Petclinic integration and Playwright acceptance.
-- [ ] 4.3 Run strict OpenSpec validation, compilation, license, diff, and working-tree checks.
+- [x] 4.1 Run foundation tests and package verification.
+- [x] 4.2 Run Petclinic integration and Playwright acceptance.
+- [x] 4.3 Run strict OpenSpec validation, compilation, license, diff, and working-tree checks.
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index 5c1d7c08a25..6f218abfd08 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -57,12 +57,16 @@ The `cw-` element namespace replaces the former `causeway-` element namespace wi
 Applications upgrading across this breaking change must replace each former opening and closing custom-element tag and each element-name selector with its one-to-one `cw-` equivalent.
 Semantic `causeway-*` events, CSS classes, `data-causeway-*` attributes, `--causeway-*` variables, and `/causeway-*` asset paths keep their existing spelling.
 
-Properties, actions, collections, and collection columns use the standard `id` attribute and native reflected `id` property to identify their Causeway member, and accept an optional `label` override.
+Properties, actions, collections, and collection columns use the standard `id` attribute and native reflected `id` property to identify their Causeway member.
+Properties, actions, and collection columns accept an optional `label` override.
+Collections accept `named` and `described-as` heading overrides; the existing `label` name override remains compatible, with `named` taking precedence.
 The former `member` attribute and custom `member` property are not compatibility aliases.
 Applications must keep authored IDs unique according to ordinary HTML identifier rules.
 A property exposes interaction only when its `editable` attribute or property is enabled.
 Object links use `logical-type`, `object-id`, `title`, and `disabled`.
 Collections use `active` and declarative collection-column children.
+Without HTML heading overrides, collections use canonical `metadata.friendlyName` and `metadata.description`, followed by a humanized member-id name and no-description fallback.
+Collection-level disabled or unmodifiable reasons are not presented as explanatory text or tooltips; individual cells and associated actions retain their own usability semantics.
 
 The corresponding JavaScript properties support programmatic composition.
 A property or collection accepts a `rendererRegistry`.
@@ -479,7 +483,7 @@ An authored property or collection can place object actions directly beneath the
   <cw-action id="updateName"></cw-action>
 </cw-property>
 
-<cw-collection id="pets" active>
+<cw-collection id="pets" named="Companion animals" described-as="Pets registered to this owner." active>
   <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>
@@ -505,7 +509,9 @@ Generated `<cw-object>` layouts retain semantically equivalent associated-member
 
 == Lazy collections and row hydration
 
-A collection's primary object requirement contains only advertised `hidden` and `disabled` state.
+A collection's primary object requirement contains advertised `hidden` and `disabled` state plus supported canonical `metadata.friendlyName` and `metadata.description` heading fields.
+The heading resolves `named`, legacy `label`, canonical friendly name, and humanized member id in that order; its description resolves `described-as`, canonical description, and no text in that order.
+A distinct description is associated with the collection and displayed below the heading in subdued smaller text, while a description equal to the resolved name is omitted.
 The `get` field is loaded through a context-owned secondary operation after activation.
 Secondary operations are cached by member and structural row selection, support forced refresh, and are canceled when their component or object context is disconnected.
 
diff --git a/viewers/webcomponents/foundation/src/collection-element.mjs b/viewers/webcomponents/foundation/src/collection-element.mjs
index b6f4f9f650b..416f59bba1f 100644
--- a/viewers/webcomponents/foundation/src/collection-element.mjs
+++ b/viewers/webcomponents/foundation/src/collection-element.mjs
@@ -69,7 +69,7 @@ export function captureDeclarativeCollectionColumns(root = globalThis.document)
 
 export class CausewayCollectionElement extends CausewayContextConsumerElement {
   static get observedAttributes() {
-    return ['id', 'label', 'active'];
+    return ['id', 'named', 'described-as', 'label', 'active'];
   }
 
   constructor() {
@@ -156,6 +156,22 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
     });
   }
 
+  get named() {
+    return this.getAttribute('named') || '';
+  }
+
+  set named(value) {
+    this.setAttribute('named', value);
+  }
+
+  get describedAs() {
+    return this.getAttribute('described-as') || '';
+  }
+
+  set describedAs(value) {
+    this.setAttribute('described-as', value);
+  }
+
   get label() {
     return this.getAttribute('label') || '';
   }
@@ -382,9 +398,9 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       return;
     }
     this.#qualifyGrid(state);
-    const label = this.label || humanize(this.id);
-    const candidateDescription = state.descriptor?.description || '';
-    const description = candidateDescription.trim().toLocaleLowerCase() === label.trim().toLocaleLowerCase()
+    const label = firstNonBlank(this.named, this.label, state.data?.metadata?.friendlyName, humanize(this.id));
+    const candidateDescription = firstNonBlank(this.describedAs, state.data?.metadata?.description);
+    const description = normalizedText(candidateDescription) === normalizedText(label)
       ? ''
       : candidateDescription;
     const shell = (content, attributes = '') => collectionShell(
@@ -399,7 +415,11 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       renderMemberPrimary(this, shell('<span role="status">Loading collection metadata…</span>', ' aria-busy="true"'));
       return;
     }
-    if (['terminal-error', 'unsupported', 'partial-error'].includes(state.status)) {
+    const recoverablePartialState = state.status === 'partial-error'
+      && state.data
+      && typeof state.data.hidden === 'boolean';
+    if (['terminal-error', 'unsupported'].includes(state.status)
+        || (state.status === 'partial-error' && !recoverablePartialState)) {
       renderMemberPrimary(this, shell(`<span class="causeway-error" role="alert">${escapeHtml(errorMessage(state))}</span>`));
       return;
     }
@@ -407,14 +427,8 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       renderMemberPrimary(this, '', {hidden: true});
       return;
     }
-    const disabledReason = typeof state.data?.disabled === 'string'
-      ? state.data.disabled
-      : state.data?.disabled === true ? 'Disabled' : '';
-    const disabledMarkup = disabledReason
-      ? `<p class="causeway-collection-disabled-reason">${escapeHtml(disabledReason)}</p>`
-      : '';
     if (!this.active) {
-      renderMemberPrimary(this, shell(`${disabledMarkup}<button type="button" data-causeway-activate>Load ${escapeHtml(label)}</button>`));
+      renderMemberPrimary(this, shell(`<button type="button" data-causeway-activate>Load ${escapeHtml(label)}</button>`));
       return;
     }
     if (this.collectionState.status === 'idle' || this.collectionState.status === 'loading') {
@@ -434,22 +448,22 @@ export class CausewayCollectionElement extends CausewayContextConsumerElement {
       ? `<p class="causeway-error" role="alert">${escapeHtml(errorMessage(this.collectionState))}</p>`
       : '';
     if (this._gridQualification.qualified) {
-      this.#renderGrid(shell, disabledMarkup, errors, description);
+      this.#renderGrid(shell, errors, description);
       return;
     }
     const content = this._columns.length > 0
       ? this.#renderTable(rows)
       : this.#renderDefaultRows(rows);
-    renderMemberPrimary(this, shell(`${disabledMarkup}${content}${errors}`));
+    renderMemberPrimary(this, shell(`${content}${errors}`));
     this.#restoreNativeFocus();
   }
 
-  #renderGrid(shell, disabledMarkup, errors, description) {
+  #renderGrid(shell, errors, description) {
     const revision = this.gridHostRevision;
     const bounded = this._gridQualification.presentation === 'grid-bounded';
     const window = this.collectionState.window;
     const pager = bounded ? this.#renderBoundedPager(window) : '';
-    renderMemberPrimary(this, shell(`${disabledMarkup}<${CAUSEWAY_COLLECTION_GRID} data-causeway-collection-grid></${CAUSEWAY_COLLECTION_GRID}>${pager}${errors}`));
+    renderMemberPrimary(this, shell(`<${CAUSEWAY_COLLECTION_GRID} data-causeway-collection-grid></${CAUSEWAY_COLLECTION_GRID}>${pager}${errors}`));
     const adapter = this.querySelector?.(`${CAUSEWAY_COLLECTION_GRID}[data-causeway-collection-grid]`);
     if (!adapter || revision !== this.gridHostRevision || !this.isConnected) return;
     adapter.presentation = {
@@ -750,6 +764,14 @@ function humanize(value) {
   return String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/^./, character => character.toUpperCase());
 }
 
+function firstNonBlank(...values) {
+  return values.find(value => String(value ?? '').trim())?.trim() ?? '';
+}
+
+function normalizedText(value) {
+  return String(value ?? '').trim().toLocaleLowerCase();
+}
+
 function collectionBreakpointPixels() {
   const candidate = globalThis.getComputedStyle?.(globalThis.document?.documentElement)?.fontSize;
   const rootFontSize = Number.parseFloat(candidate);
diff --git a/viewers/webcomponents/foundation/src/object-context-controller.mjs b/viewers/webcomponents/foundation/src/object-context-controller.mjs
index ba8c6579761..10e7ae474ce 100644
--- a/viewers/webcomponents/foundation/src/object-context-controller.mjs
+++ b/viewers/webcomponents/foundation/src/object-context-controller.mjs
@@ -1301,9 +1301,15 @@ function translateRequirement(requirement, description) {
     throw new Error(`Collection '${requirement.member}' does not expose readable contents.`);
   }
   const supportedFields = ['hidden', 'disabled'].filter(field => member.fields.has(field));
+  const memberSelection = Object.fromEntries(supportedFields.map(field => [field, true]));
+  const metadataFields = ['friendlyName', 'description']
+    .filter(field => member.metadata?.fields.has(field));
+  if (metadataFields.length > 0) {
+    memberSelection.metadata = Object.fromEntries(metadataFields.map(field => [field, true]));
+  }
   return {
     descriptor: member,
-    selection: {[member.id]: Object.fromEntries(supportedFields.map(field => [field, true]))}
+    selection: {[member.id]: memberSelection}
   };
 }
 
diff --git a/viewers/webcomponents/foundation/src/theme.css b/viewers/webcomponents/foundation/src/theme.css
index 23a59da2914..2ea8f618b06 100644
--- a/viewers/webcomponents/foundation/src/theme.css
+++ b/viewers/webcomponents/foundation/src/theme.css
@@ -355,13 +355,19 @@ cw-property:last-child .causeway-property {
   text-align: start;
 }
 
-.causeway-property-description,
-.causeway-collection-description {
+.causeway-property-description {
   grid-column: 2 / -1;
   color: var(--causeway-muted);
   font-size: var(--causeway-font-size-sm);
 }
 
+.causeway-collection-description {
+  margin: 0;
+  padding: var(--causeway-space-2) var(--causeway-space-4);
+  color: var(--causeway-muted);
+  font-size: var(--causeway-font-size-sm);
+}
+
 :where(cw-property[multiline]) .causeway-property-label {
   grid-column: 1;
   grid-row: 1;
diff --git a/viewers/webcomponents/foundation/test/collection.test.mjs b/viewers/webcomponents/foundation/test/collection.test.mjs
index 972b2479023..e27e440322c 100644
--- a/viewers/webcomponents/foundation/test/collection.test.mjs
+++ b/viewers/webcomponents/foundation/test/collection.test.mjs
@@ -558,6 +558,87 @@ test('collection component does not read until activated and renders declared co
   assert.equal(collection.hidden, true);
 });
 
+test('collection component resolves canonical and HTML headings without unmodifiable noise or rereads', async () => {
+  let registrations = 0;
+  let publish;
+  const context = {
+    registerRequirement(_requirement, listener) {
+      registrations += 1;
+      publish = listener;
+      listener({
+        status: 'ready',
+        descriptor: {id: 'staffMembers'},
+        data: {
+          hidden: false,
+          disabled: 'Cannot edit a mixed-in collection.',
+          metadata: {friendlyName: 'Department staff', description: 'Current staff members.'}
+        },
+        errors: [],
+        generation: 1
+      });
+      return () => {};
+    }
+  };
+  const collection = new CausewayCollectionElement();
+  collection.id = 'staffMembers';
+  collection.context = context;
+  document.body.appendChild(collection);
+  await waitFor(() => collection.innerHTML.includes('Department staff'));
+
+  assert.deepEqual(CausewayCollectionElement.observedAttributes, ['id', 'named', 'described-as', 'label', 'active']);
+  assert.match(collection.innerHTML, /class="causeway-collection-label">Department staff<\/h2>/);
+  assert.match(collection.innerHTML, /class="causeway-collection-description">Current staff members\.<\/p>/);
+  assert.match(collection.innerHTML, /aria-labelledby="causeway-collection-label-/);
+  assert.match(collection.innerHTML, /aria-describedby="causeway-collection-description-/);
+  assert.doesNotMatch(collection.innerHTML, /Cannot edit a mixed-in collection/);
+  assert.doesNotMatch(collection.innerHTML, /causeway-collection-disabled-reason/);
+
+  publish({
+    status: 'partial-error',
+    descriptor: {id: 'staffMembers'},
+    data: {
+      hidden: false,
+      disabled: null,
+      metadata: {friendlyName: 'Department staff', description: null}
+    },
+    errors: [{message: 'Collection description is unavailable.'}],
+    generation: 2
+  });
+  assert.match(collection.innerHTML, /Department staff/);
+  assert.doesNotMatch(collection.innerHTML, /causeway-collection-description/);
+  assert.doesNotMatch(collection.innerHTML, /causeway-error/);
+  publish({
+    status: 'ready',
+    descriptor: {id: 'staffMembers'},
+    data: {
+      hidden: false,
+      disabled: null,
+      metadata: {friendlyName: 'Department staff', description: 'Current staff members.'}
+    },
+    errors: [],
+    generation: 3
+  });
+
+  collection.label = 'Legacy staff label';
+  assert.match(collection.innerHTML, /Legacy staff label/);
+  collection.named = 'Priority team';
+  collection.describedAs = 'People assigned to priority cases.';
+  assert.match(collection.innerHTML, /class="causeway-collection-label">Priority team<\/h2>/);
+  assert.match(collection.innerHTML, /class="causeway-collection-description">People assigned to priority cases\.<\/p>/);
+
+  collection.describedAs = ' priority TEAM ';
+  assert.doesNotMatch(collection.innerHTML, /causeway-collection-description/);
+  assert.doesNotMatch(collection.innerHTML, /aria-describedby/);
+
+  collection.removeAttribute('named');
+  collection.removeAttribute('described-as');
+  collection.removeAttribute('label');
+  assert.match(collection.innerHTML, /Department staff/);
+  assert.match(collection.innerHTML, /Current staff members/);
+  assert.equal(registrations, 1);
+  document.body.removeChild(collection);
+});
+
 test('collection component forwards window requests and publishes semantic window state', async () => {
   let request;
   const context = {
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index a4906d8218f..1bbe5cef39f 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -56,6 +56,12 @@ test('disabled reasons use label-owned tooltips and string values align to logic
   assert.match(theme, /\.causeway-property-value-string \{\s+text-align: start;/);
 });
 
+test('collection descriptions remain quiet text directly below their labels', async () => {
+  const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
+  assert.match(theme, /\.causeway-collection-description \{\s+margin: 0;\s+padding: var\(--causeway-space-2\) var\(--causeway-space-4\);\s+color: var\(--causeway-muted\);\s+font-size: var\(--causeway-font-size-sm\);/);
+  assert.doesNotMatch(theme, /\.causeway-property-description,\s+\.causeway-collection-description/);
+});
+
 test('application native-control chrome does not cross into toolkit-owned slotted field inputs', async () => {
   const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
   const nativeInput = 'input:not([slot="input"])';
diff --git a/viewers/webcomponents/foundation/test/fixtures/rich-schema-fixture.mjs b/viewers/webcomponents/foundation/test/fixtures/rich-schema-fixture.mjs
index 05b6645f0bc..f16a1d748c8 100644
--- a/viewers/webcomponents/foundation/test/fixtures/rich-schema-fixture.mjs
+++ b/viewers/webcomponents/foundation/test/fixtures/rich-schema-fixture.mjs
@@ -113,12 +113,14 @@ export function createRichSchemaTypes() {
     [COLLECTION_TYPE, objectType(COLLECTION_TYPE, null, [
       field('hidden', null, scalar('Boolean')),
       field('disabled', null, scalar('String')),
+      field('metadata', null, named(MEMBER_METADATA_TYPE)),
       field('get', null, list(named(STAFF_OBJECT_TYPE))),
       field('datatype', null, scalar('String'))
     ])],
     [EMPTY_COLLECTION_TYPE, objectType(EMPTY_COLLECTION_TYPE, null, [
       field('hidden', null, scalar('Boolean')),
       field('disabled', null, scalar('String')),
+      field('metadata', null, named(MEMBER_METADATA_TYPE)),
       field('get', null, list(named(STAFF_OBJECT_TYPE))),
       field('datatype', null, scalar('String'))
     ])],
@@ -301,8 +303,16 @@ export function departmentObjectData({
       get: {name: 'history.txt', mimeType: 'text/plain', chars: '/graphql/object/history/clobChars'}
     },
     unsupportedValue: {hidden: false, disabled: null, get: {nested: {value: 'unknown'}}},
-    staffMembers: {hidden: false, disabled: null},
-    formerStaff: {hidden: false, disabled: null},
+    staffMembers: {
+      hidden: false,
+      disabled: null,
+      metadata: {friendlyName: 'Department staff', description: 'Current staff members.'}
+    },
+    formerStaff: {
+      hidden: false,
+      disabled: null,
+      metadata: {friendlyName: 'Former staff', description: null}
+    },
     changeName: {hidden: false, disabled: null}
   };
 }
diff --git a/viewers/webcomponents/foundation/test/member-composition.test.mjs b/viewers/webcomponents/foundation/test/member-composition.test.mjs
index 165cfe6948f..e79cb13c804 100644
--- a/viewers/webcomponents/foundation/test/member-composition.test.mjs
+++ b/viewers/webcomponents/foundation/test/member-composition.test.mjs
@@ -191,7 +191,9 @@ test('collection keeps interleaved columns and actions in separate vocabularies'
   assert.equal(collection.childNodes[0].hidden, true);
   assert.equal(addPet.isConnected, true);
   context.publish('collection:pets', collectionState({disabled: 'Collection is locked'}));
-  assert.match(collection.innerHTML, /Collection is locked/);
+  assert.doesNotMatch(collection.innerHTML, /Collection is locked/);
+  assert.equal(addPet.isConnected, true);
+  assert.equal(removePet.isConnected, true);
   assert.deepEqual(collection.columns.map(candidate => candidate.member), ['name', 'species']);
   assert.equal(context.count('action:addPet'), 1);
   assert.equal(context.count('action:removePet'), 1);
diff --git a/viewers/webcomponents/foundation/test/object-context.test.mjs b/viewers/webcomponents/foundation/test/object-context.test.mjs
index 953f4fffa56..67569c9397f 100644
--- a/viewers/webcomponents/foundation/test/object-context.test.mjs
+++ b/viewers/webcomponents/foundation/test/object-context.test.mjs
@@ -133,6 +133,56 @@ test('property reads remain compatible when member metadata is unavailable', asy
   assert.equal(propertyState.data.get, 'Classics');
 });
 
+test('collection reads include only supported canonical heading metadata', async () => {
+  const executor = createRichSchemaFixtureExecutor();
+  const context = createContext(executor);
+  let collectionState;
+  context.registerRequirement({kind: 'collection', member: 'staffMembers'}, state => { collectionState = state; });
+  await waitFor(() => context.state.status === 'ready');
+
+  assert.equal(executor.readCalls.length, 1);
+  const collectionDocument = executor.readCalls[0].document;
+  assert.match(collectionDocument, /staffMembers\s*\{[\s\S]*metadata\s*\{/);
+  assert.match(collectionDocument, /\bfriendlyName\b/);
+  assert.match(collectionDocument, /\bdescription\b/);
+  assert.deepEqual(collectionState.data.metadata, {
+    friendlyName: 'Department staff',
+    description: 'Current staff members.'
+  });
+
+  const metadataErrorResponse = graphQLObjectResponse();
+  metadataErrorResponse.data.rich[DEPARTMENT_OBJECT_FIELD].staffMembers.metadata.description = null;
+  metadataErrorResponse.errors = [{
+    message: 'Collection description is unavailable.',
+    path: ['rich', DEPARTMENT_OBJECT_FIELD, 'staffMembers', 'metadata', 'description'],
+    extensions: {classification: 'DataFetchingException'}
+  }];
+  const metadataErrorExecutor = createRichSchemaFixtureExecutor({readResponses: [metadataErrorResponse]});
+  const metadataErrorContext = createContext(metadataErrorExecutor);
+  let metadataErrorState;
+  metadataErrorContext.registerRequirement({kind: 'collection', member: 'staffMembers'}, state => { metadataErrorState = state; });
+  await waitFor(() => metadataErrorContext.state.status === 'partial-error');
+  assert.equal(metadataErrorState.status, 'partial-error');
+  assert.equal(metadataErrorState.data.metadata.friendlyName, 'Department staff');
+  assert.equal(metadataErrorState.data.metadata.description, null);
+
+  const types = createRichSchemaTypes();
+  for (const [name, type] of types) {
+    if (type.fields?.some(field => field.name === 'friendlyName')) {
+      types.set(name, {...type, fields: type.fields.filter(field => field.name !== 'description')});
+    }
+  }
+  const partialExecutor = createRichSchemaFixtureExecutor({types});
+  const partialContext = createContext(partialExecutor);
+  partialContext.registerRequirement({kind: 'collection', member: 'staffMembers'});
+  await waitFor(() => partialContext.state.status === 'ready');
+  const collectionSelection = partialExecutor.readCalls[0].document.slice(
+    partialExecutor.readCalls[0].document.indexOf('staffMembers {')
+  );
+  assert.match(collectionSelection, /metadata\s*\{[\s\S]*friendlyName/);
+  assert.doesNotMatch(collectionSelection, /\bdescription\b/);
+});
+
 test('coalesces layout metadata with semantic member requirements', async () => {
   const executor = createRichSchemaFixtureExecutor();
   const context = createContext(executor);
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-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.java
index 0851b3203da..01083d9e3fb 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-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetClinicHomePage.java
@@ -60,7 +60,9 @@ public List<PetOwner> getPetOwners() {
     }
 
     @Collection
-    @CollectionLayout(paged = 10)
+    @CollectionLayout(
+            paged = 10,
+            describedAs = "Scheduled visits that have not yet taken place.")
     public List<Visit> getFutureVisits() {
         final LocalDateTime now = clockService.getClock().nowAsLocalDateTime();
         return visitRepository.findByVisitAtAfter(now);
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-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.java
index 792497b5f48..3329f504227 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-htmx-petclinic/src/main/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/domain/PetOwner.java
@@ -110,7 +110,9 @@ public class PetOwner implements Comparable<PetOwner> {
     private LocalDate lastVisit;
 
     @Collection
-    @CollectionLayout(sortedBy = Pet.ByName.class)
+    @CollectionLayout(
+            sortedBy = Pet.ByName.class,
+            describedAs = "Pets currently registered to this owner.")
     @OneToMany(mappedBy = "petOwner", cascade = CascadeType.ALL, orphanRemoval = true)
     private Set<Pet> pets = new TreeSet<>();
 
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 c065e60886d..4549c8529e0 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
@@ -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" label="Upcoming visits" active offset="0" size="10">
+      <cw-collection id="futureVisits" named="Next appointments" active offset="0" size="10">
         <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 cc516415d6c..3c8a91e595b 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" label="Pets" active offset="0" size="10">
+        <cw-collection id="pets" named="Companion animals" active offset="0" size="10">
           <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>
@@ -66,7 +66,9 @@ <h2 id="petclinic-owner-pets-heading">Pets</h2>
 
       <section class="petclinic-card" aria-labelledby="petclinic-owner-visits-heading">
         <h2 id="petclinic-owner-visits-heading">Visits</h2>
-        <cw-collection id="visits" label="Visits" active offset="0" size="10">
+        <cw-collection id="visits" named="Visit history"
+                       described-as="All visits recorded for this owner's pets."
+                       active offset="0" size="10">
           <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 896fabcb33e..b8fee8c5d7c 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
@@ -199,6 +199,12 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         assertFocused(ROUTE_PAGE);
         waitForCollectionRows("petOwners", 4);
         waitForCollectionRows("futureVisits", 3);
+        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())
+                .isEqualTo("Scheduled visits that have not yet taken place.");
+        assertThat(page.locator("cw-collection[id='petOwners'] .causeway-collection-description").count())
+                .isZero();
         assertCollectionPresentation("petOwners", "narrow");
         assertCollectionPresentation("futureVisits", "narrow");
         assertThat(toolkitRequests.stream().filter(url -> url.contains("/vaadin-menubar/vaadin-menubar.js")).count())
@@ -223,6 +229,12 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
         assertFocused(ROUTE_PAGE);
         waitForCollectionRows("pets", 2);
         waitForCollectionRows("visits", 2);
+        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();
+        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);
@@ -1035,6 +1047,17 @@ private void waitForCollectionRows(final String member, final int count) {
         }
     }
 
+    private void assertCollectionHeading(
+            final String member,
+            final String expectedName,
+            final String expectedDescription) {
+        final var collection = page.locator("cw-collection[id='" + member + "']");
+        assertThat(collection.locator(".causeway-collection-label").innerText()).isEqualTo(expectedName);
+        assertThat(collection.locator(".causeway-collection-description").innerText()).isEqualTo(expectedDescription);
+        assertThat(collection.locator(".causeway-collection").getAttribute("aria-labelledby")).isNotBlank();
+        assertThat(collection.locator(".causeway-collection").getAttribute("aria-describedby")).isNotBlank();
+    }
+
     private void assertCollectionPresentation(final String member, final String expected) {
         final var collection = page.locator("cw-collection[id='" + member + "']");
         if (nativeToolkit() || !"grid".equals(expected)) {
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 bc83c37d7ef..04a2291ccf8 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,12 +216,22 @@ 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\"")
+                .contains("<cw-collection id=\"pets\" named=\"Companion animals\"")
                 .contains("<cw-action id=\"addPet\"")
                 .contains("<cw-action id=\"removePet\"")
-                .contains("<cw-collection id=\"visits\"")
+                .contains("<cw-collection id=\"visits\" named=\"Visit history\"")
+                .contains("described-as=\"All visits recorded for this owner's pets.\"")
                 .contains("<cw-action id=\"bookVisit\"")
                 .doesNotContain("petclinic-associated-actions", "petclinic-member-composition");
+        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=");
         assertThat(ownerHtml.indexOf("id=\"addPet\""))
                 .isLessThan(ownerHtml.indexOf("id=\"removePet\""));
         assertThat(get("/META-INF/causeway/webcomponents/pages/petclinic.PetOwner.html").statusCode())
@@ -319,8 +329,8 @@ void exposesPetclinicServiceAndObjectInteractionsThroughRichGraphQL() throws Exc
                     petclinic_PetOwner(object: {id: "s_owner-mary"}) {
                       _meta { id logicalTypeName title grid }
                       name { get }
-                      pets { get { _meta { id logicalTypeName title } } }
-                      visits { get { _meta { id logicalTypeName title } } }
+                      pets { metadata { friendlyName description } get { _meta { id logicalTypeName title } } }
+                      visits { metadata { friendlyName description } get { _meta { id logicalTypeName title } } }
                       addPet { hidden disabled params { name { validity datatype } } }
                       bookVisit { hidden disabled }
                     }
@@ -329,6 +339,11 @@ void exposesPetclinicServiceAndObjectInteractionsThroughRichGraphQL() throws Exc
                 """);
         assertNoGraphQLErrors(owner);
         assertThat(owner.at("/data/rich/petclinic_PetOwner/name/get").asText()).isEqualTo("Mary Smith");
+        assertThat(owner.at("/data/rich/petclinic_PetOwner/pets/metadata/friendlyName").asText()).isEqualTo("Pets");
+        assertThat(owner.at("/data/rich/petclinic_PetOwner/pets/metadata/description").asText())
+                .isEqualTo("Pets currently registered to this owner.");
+        assertThat(owner.at("/data/rich/petclinic_PetOwner/visits/metadata/friendlyName").asText()).isEqualTo("Visits");
+        assertThat(owner.at("/data/rich/petclinic_PetOwner/visits/metadata/description").isNull()).isTrue();
         assertThat(owner.at("/data/rich/petclinic_PetOwner/pets/get").size()).isEqualTo(2);
         assertThat(owner.at("/data/rich/petclinic_PetOwner/visits/get").size()).isEqualTo(2);
     }


(causeway) 01/06: docs(openspec): propose collection headings

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 61e126c61fdd207b5ba69a6dacdb2aa57a0da2dc
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 15:37:29 2026 +0200

    docs(openspec): propose collection headings
---
 .../enhance-collection-headings/.openspec.yaml     |  2 +
 .../changes/enhance-collection-headings/design.md  | 67 ++++++++++++++++++++++
 .../enhance-collection-headings/proposal.md        | 32 +++++++++++
 .../specs/domain-web-components/spec.md            | 42 ++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 27 +++++++++
 .../specs/graphql-web-component-context/spec.md    | 19 ++++++
 .../changes/enhance-collection-headings/tasks.md   | 24 ++++++++
 7 files changed, 213 insertions(+)

diff --git a/openspec/changes/enhance-collection-headings/.openspec.yaml b/openspec/changes/enhance-collection-headings/.openspec.yaml
new file mode 100644
index 00000000000..50adc91034f
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-29
diff --git a/openspec/changes/enhance-collection-headings/design.md b/openspec/changes/enhance-collection-headings/design.md
new file mode 100644
index 00000000000..6db46b7318a
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/design.md
@@ -0,0 +1,67 @@
+## Context
+
+`<cw-collection>` already renders a semantic heading shell and has styling for a smaller collection description, but it derives its heading from the legacy HTML `label` attribute or the member id and does not select canonical collection metadata.
+The component also turns the collection wrapper's disabled reason into visible prose, producing messages such as “Cannot edit a mixed-in collection” even though collection contents remain useful and readable.
+Rich GraphQL wrappers already expose canonical `metadata.friendlyName` and `metadata.description`, so no schema expansion is required.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Support application-authored collection names and descriptions with `named` and `described-as`.
+- Use canonical collection friendly names and descriptions when HTML does not override them.
+- Present descriptions below headings with quiet, smaller typography and accessible association.
+- Remove visible collection-level unmodifiable noise without changing read-only semantics.
+- Demonstrate selective combinations in Petclinic.
+
+**Non-Goals:**
+
+- Make collections editable.
+- Hide disabled reasons for properties, actions, parameters, or associated collection actions.
+- Change Rich GraphQL metadata fields or metamodel facet semantics.
+- Add toolkit-specific collection heading behavior.
+
+## Decisions
+
+### Resolve names and descriptions with explicit precedence
+
+The name precedence is `named`, legacy `label`, canonical `metadata.friendlyName`, then a humanized member id.
+The description precedence is `described-as`, canonical `metadata.description`, then no description.
+Blank explicit values are treated as absent, and a description equal to the resolved name after trimmed case-insensitive comparison is suppressed.
+
+Keeping `label` as a compatibility alias avoids breaking existing pages while making `named` the preferred domain-language attribute.
+Using light-DOM heading slots was rejected because the existing generated shell owns loading, error, empty, table, and Grid states consistently.
+
+### Select collection metadata through the existing requirement
+
+Collection requirement translation will request only supported `friendlyName` and `description` fields under the member's `metadata` wrapper.
+The component reads accepted values from the current response data rather than from schema introspection descriptors.
+This follows property metadata behavior and preserves targeted, additive GraphQL reads.
+
+### Keep disabled state but omit collection-level explanation markup
+
+The collection continues receiving hidden and disabled wrapper state and remains read-only.
+It will not render the collection wrapper's disabled boolean or reason as text, title, or tooltip.
+Member cells and associated actions retain their own authorization and usability semantics.
+
+### Retain one accessible heading shell
+
+The existing `<h2>` and optional following description remain the collection's accessible name and description through `aria-labelledby` and `aria-describedby`.
+CSS will give the description explicit inline padding, subdued color, smaller text, and spacing directly below the label before collection content.
+
+## Risks / Trade-offs
+
+- [Risk] Existing pages rely on visible collection disabled reasons. → Limit suppression to the collection wrapper; actionable controls retain their own reasons.
+- [Risk] Additional metadata broadens every selected collection query. → Select only fields supported by introspection and reuse the current bounded member read.
+- [Risk] Two HTML name attributes could conflict. → Define deterministic `named` precedence and retain `label` only for compatibility.
+- [Risk] Duplicate descriptions add noise. → Suppress descriptions equal to the resolved name.
+
+## Migration Plan
+
+No migration is required.
+Existing `label` attributes continue to work, while pages can adopt `named` and `described-as` incrementally.
+Rollback removes the two attributes, metadata selection, Petclinic examples, and collection-specific presentation changes.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/enhance-collection-headings/proposal.md b/openspec/changes/enhance-collection-headings/proposal.md
new file mode 100644
index 00000000000..fa0d73c0f56
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/proposal.md
@@ -0,0 +1,32 @@
+## Why
+
+Collection cards currently cannot express application-authored names and descriptions using the domain vocabulary requested by HTML page authors, and their visible unmodifiable reasons add noise without helping users interact with read-only collection contents.
+Collection headings should consistently present either canonical metamodel text or explicit HTML overrides, while Petclinic demonstrates both paths selectively.
+
+## What Changes
+
+- Add `named` and `described-as` attributes to `<cw-collection>` for application-authored heading overrides.
+- Load canonical collection `friendlyName` and `description` metadata through the existing targeted object context.
+- Resolve collection heading text with explicit HTML overrides taking precedence over canonical metadata and safe fallbacks.
+- Render a non-duplicate collection description directly below its label using subdued, smaller typography.
+- Stop rendering collection-level disabled or unmodifiable reasons as visible labels or tooltips while retaining read-only collection behavior.
+- Add selective Petclinic collection names and descriptions, using both `@CollectionLayout(describedAs)` and HTML `described-as` examples.
+- Add component, context, GraphQL integration, Petclinic integration, and browser regression coverage.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `domain-web-components`: Extend collection heading semantics, HTML overrides, description presentation, and quiet read-only behavior.
+- `graphql-web-component-context`: Select supported canonical collection heading metadata in targeted member reads.
+- `generic-htmx-web-component-viewer`: Demonstrate and verify selective collection names and descriptions in Petclinic.
+
+## Impact
+
+Affected code is limited to the framework-owned collection component and styles, targeted GraphQL selection state, existing test fixtures, and Petclinic domain/page examples.
+The change adds no dependency, route, toolkit, persistence, or shared application-shell behavior.
+Existing `label` markup remains compatible, while `named` is the preferred collection-name override.
diff --git a/openspec/changes/enhance-collection-headings/specs/domain-web-components/spec.md b/openspec/changes/enhance-collection-headings/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..cfcd5fe9aa4
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/specs/domain-web-components/spec.md
@@ -0,0 +1,42 @@
+## ADDED Requirements
+
+### Requirement: Semantic collection headings
+The `<cw-collection>` component SHALL render one semantic collection heading from an explicit HTML name, canonical member metadata, or a safe member-id fallback.
+It SHALL render a distinct available description directly below that heading in subdued, smaller text and associate both texts with every collection presentation state.
+
+#### Scenario: HTML supplies both overrides
+- **WHEN** a collection declares non-blank `named` and `described-as` attributes
+- **THEN** those values are the rendered collection name and description
+- **AND** they override canonical friendly-name and description metadata
+
+#### Scenario: Canonical metadata supplies heading text
+- **WHEN** no applicable HTML override exists and the collection response supplies canonical `metadata.friendlyName` or `metadata.description`
+- **THEN** each available value is used independently
+- **AND** a missing value falls back to a humanized member id for the name or no description for the description
+
+#### Scenario: Legacy label remains present
+- **WHEN** a collection declares the existing `label` attribute without `named`
+- **THEN** the label remains the effective name for backward compatibility
+- **AND** `named` takes precedence when both are non-blank
+
+#### Scenario: Description duplicates resolved name
+- **WHEN** the candidate description equals the resolved name after trimming and case-insensitive comparison
+- **THEN** no duplicate description is rendered or referenced
+
+#### Scenario: Heading attributes change
+- **WHEN** `named`, `described-as`, `label`, or the member id changes while the component is connected
+- **THEN** the current collection shell rerenders with the newly resolved accessible heading
+- **AND** collection data is not independently reloaded solely for the text change
+
+### Requirement: Quiet read-only collection presentation
+Collection wrappers SHALL remain read-only and authorization-aware without rendering their disabled or unmodifiable boolean or reason as visible explanatory text, a label, or a tooltip.
+
+#### Scenario: Mixed-in collection is unmodifiable
+- **WHEN** a readable mixed-in collection reports a disabled reason such as “Cannot edit a mixed-in collection”
+- **THEN** its rows, heading, description, paging, and applicable associated actions remain available
+- **AND** the collection-level reason is not rendered as text or tooltip
+
+#### Scenario: Member or associated action is disabled
+- **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 suppression does not broaden authorization or enable mutation
diff --git a/openspec/changes/enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
new file mode 100644
index 00000000000..412d61d96c7
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
@@ -0,0 +1,27 @@
+## ADDED Requirements
+
+### Requirement: Petclinic collection heading demonstrations
+The Petclinic HTMX application SHALL selectively demonstrate canonical collection descriptions, HTML-authored collection names, and HTML-authored collection description overrides without requiring application-specific component code.
+
+#### Scenario: Domain facet supplies a description
+- **WHEN** a Petclinic collection annotated with `@CollectionLayout(describedAs)` is rendered without an HTML description override
+- **THEN** its canonical description appears below the effective collection name
+- **AND** collections without a description remain free of placeholder prose
+
+#### Scenario: HTML supplies collection heading overrides
+- **WHEN** a Petclinic resource page declares `named` or `described-as` on selected `<cw-collection>` elements
+- **THEN** those explicit values appear for those collections only
+- **AND** the same component continues using canonical metadata or fallbacks for other collections
+
+#### Scenario: Petclinic owner visits are rendered
+- **WHEN** the mixed-in owner visits collection loads
+- **THEN** its selected name and description are visible and accessible
+- **AND** “Cannot edit a mixed-in collection” or an equivalent collection-level tooltip is absent
+
+### Requirement: Petclinic collection heading regression coverage
+Petclinic integration and browser acceptance SHALL verify canonical and HTML-authored collection heading combinations, quiet read-only collection presentation, and unaffected collection navigation and actions.
+
+#### Scenario: Petclinic heading combinations are exercised
+- **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
diff --git a/openspec/changes/enhance-collection-headings/specs/graphql-web-component-context/spec.md b/openspec/changes/enhance-collection-headings/specs/graphql-web-component-context/spec.md
new file mode 100644
index 00000000000..43526b5dd70
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/specs/graphql-web-component-context/spec.md
@@ -0,0 +1,19 @@
+## ADDED Requirements
+
+### Requirement: Targeted collection heading metadata
+A semantic collection requirement SHALL select supported canonical `friendlyName` and `description` fields from the collection wrapper's existing metadata object alongside collection visibility and usability state.
+
+#### Scenario: Collection metadata fields are supported
+- **WHEN** targeted introspection finds `metadata.friendlyName` or `metadata.description` on a requested collection wrapper
+- **THEN** the object-context selection includes only the supported heading metadata fields
+- **AND** accepted response metadata is delivered with the current collection requirement state
+
+#### Scenario: Collection metadata is unavailable or partially erroneous
+- **WHEN** the wrapper lacks either heading field or GraphQL reports a nullable field-local error
+- **THEN** the remaining supported collection state stays usable
+- **AND** the component can apply explicit HTML overrides or safe fallbacks without a complete-schema read
+
+#### Scenario: Collection heading attribute changes
+- **WHEN** an HTML-only name or description override changes after metadata has loaded
+- **THEN** the component reuses the current requirement state
+- **AND** no extra GraphQL request is issued solely for presentation text
diff --git a/openspec/changes/enhance-collection-headings/tasks.md b/openspec/changes/enhance-collection-headings/tasks.md
new file mode 100644
index 00000000000..3f0bb0b01d8
--- /dev/null
+++ b/openspec/changes/enhance-collection-headings/tasks.md
@@ -0,0 +1,24 @@
+## 1. Collection metadata and heading behavior
+
+- [ ] 1.1 Extend targeted collection requirements to select supported canonical friendly-name and description metadata.
+- [ ] 1.2 Implement `named` and `described-as` collection attributes with documented precedence, compatibility, duplicate suppression, and reactive rerendering.
+- [ ] 1.3 Remove visible collection-level disabled or unmodifiable reason markup while preserving read-only and member/action semantics.
+- [ ] 1.4 Refine collection description styling below the label with accessible subdued typography.
+
+## 2. Foundation regression coverage
+
+- [ ] 2.1 Add object-context tests for targeted collection metadata selection, partial support, and request reuse.
+- [ ] 2.2 Add collection component tests for canonical and HTML heading precedence, reactive attributes, accessibility, duplicate suppression, and quiet unmodifiable state.
+- [ ] 2.3 Update stylesheet and component-contract coverage for the new public attributes and description presentation.
+
+## 3. Petclinic demonstrations
+
+- [ ] 3.1 Add selective `@CollectionLayout(describedAs)` descriptions to Petclinic collections.
+- [ ] 3.2 Add selective `named` and `described-as` overrides to Petclinic HTML pages while leaving other collections metadata-driven or unchanged.
+- [ ] 3.3 Extend Petclinic integration and Playwright assertions for names, descriptions, omitted unmodifiable noise, and unaffected interactions.
+
+## 4. Validation and archive readiness
+
+- [ ] 4.1 Run foundation tests and package verification.
+- [ ] 4.2 Run Petclinic integration and Playwright acceptance.
+- [ ] 4.3 Run strict OpenSpec validation, compilation, license, diff, and working-tree checks.


(causeway) 05/06: fix(webcomponents): align narrow multiline properties

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 c8384ec734d3b1765de717fb0c8c1ba95b675b6c
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 16:19:51 2026 +0200

    fix(webcomponents): align narrow multiline properties
---
 .../fix-narrow-multiline-property-layout/design.md | 14 ++++----
 .../specs/domain-web-components/spec.md            |  4 +--
 .../fix-narrow-multiline-property-layout/tasks.md  | 16 ++++-----
 .../foundation/src/property-element.mjs            | 13 +++++---
 viewers/webcomponents/foundation/src/theme.css     | 38 ++++++++--------------
 .../foundation/test/component-styles.test.mjs      | 13 ++++----
 .../foundation/test/components.test.mjs            | 37 +++++++++++++++++++++
 .../foundation/test/interaction.test.mjs           |  2 ++
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |  4 ++-
 9 files changed, 89 insertions(+), 52 deletions(-)

diff --git a/openspec/changes/fix-narrow-multiline-property-layout/design.md b/openspec/changes/fix-narrow-multiline-property-layout/design.md
index 15fa3d7074f..917b93f82e8 100644
--- a/openspec/changes/fix-narrow-multiline-property-layout/design.md
+++ b/openspec/changes/fix-narrow-multiline-property-layout/design.md
@@ -3,7 +3,8 @@
 `CausewayPropertyElement` correctly resolves multiline depth from canonical `multi-line`, legacy `multiline`, or GraphQL `metadata.multiLine`.
 Its editor receives that effective value, but the theme's layout selectors inspect only `cw-property[multiline]`.
 For a canonical Petclinic `multi-line="5"` property, the multiline row rules therefore do not match.
-At narrow width, generic grid declarations move the value but leave the edit control auto-placed, producing an implicit-grid offset of roughly 2.4 pixels and failing the explicit alignment assertion.
+At narrow width, theme declarations target the nested value and edit control even though the direct grid item is their `.causeway-property-field` wrapper.
+The wrapper therefore retains incompatible wide placement while its differently sized flex children remain center-aligned, producing the observed roughly 2.4-pixel top offset and failing the explicit alignment assertion.
 
 ## Goals / Non-Goals
 
@@ -31,13 +32,14 @@ The shell is regenerated for loading, error, view, disabled, and edit states, so
 Selecting both host attribute spellings in CSS was rejected because it would still miss metadata-only multiline state.
 Reflecting metadata back onto application-authored `<cw-property>` was rejected because internal presentation should not mutate public source declarations.
 
-### Select the framework-owned shell in layout CSS
+### Select the framework-owned shell and direct field grid item
 
-Wide, container-narrow, and viewport-narrow multiline rules will target `.causeway-property[data-multi-line]` and explicitly place label, description, value, and edit control.
-This uses the element that is actually the grid container and avoids dependence on ancestor attribute spelling.
+Wide, container-narrow, and viewport-narrow multiline rules will target `.causeway-property[data-multi-line]` and explicitly place the direct label, description, and `.causeway-property-field` grid items.
+At narrow width, the field occupies the explicit third row across the available columns and top-aligns its value and bounded edit control.
+This uses the element that is actually the grid container and the child that is actually its grid item, avoiding dependence on ancestor attribute spelling or ineffective rules for nested flex children.
 
-Increasing geometry tolerance was rejected because the failure exposed auto-placement rather than harmless rounding.
-Adding a cosmetic edit-button margin was rejected because it would mask the missing grid-row contract and could misalign other states.
+Increasing geometry tolerance was rejected because the failure exposed incorrect wrapper placement and alignment rather than harmless rounding.
+Adding a cosmetic edit-button margin was rejected because it would mask the missing grid-item contract and could misalign other states.
 
 ### Assert effective state and geometry
 
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md b/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
index 2ee584a2e1d..e3d49b78ef3 100644
--- a/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
+++ b/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
@@ -30,6 +30,6 @@ Effective multiline presentation from canonical HTML, compatibility HTML, or mem
 
 #### Scenario: Narrow presentation stacks safely
 - **WHEN** a multiline `LEFT` property resolved from `multi-line`, legacy `multiline`, or `metadata.multiLine` cannot retain its configured columns at a narrow inline size
-- **THEN** label, description, field, and controls occupy explicit successive grid rows in meaningful document order
-- **AND** the edit control aligns with its field without overlap or implicit-grid displacement
+- **THEN** label and description occupy explicit successive rows followed by an explicit full-width field row in meaningful document order
+- **AND** the field's value and bounded edit control align at the start of that row without overlap or implicit-grid displacement
 - **AND** the property introduces no horizontal overflow or clipping
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/tasks.md b/openspec/changes/fix-narrow-multiline-property-layout/tasks.md
index 2ef9a34ae05..8abb650ff24 100644
--- a/openspec/changes/fix-narrow-multiline-property-layout/tasks.md
+++ b/openspec/changes/fix-narrow-multiline-property-layout/tasks.md
@@ -1,16 +1,16 @@
 ## 1. Effective multiline shell state
 
-- [ ] 1.1 Add a generated property-shell attribute derived from effective canonical, compatibility, or metadata multiline state in every render state.
-- [ ] 1.2 Replace host-spelling-dependent multiline selectors with shell-state selectors while preserving wide and narrow grid placement.
+- [x] 1.1 Add a generated property-shell attribute derived from effective canonical, compatibility, or metadata multiline state in every render state.
+- [x] 1.2 Replace host-spelling-dependent multiline selectors with shell-state selectors while preserving wide and narrow grid placement.
 
 ## 2. Regression coverage
 
-- [ ] 2.1 Add property component tests for canonical, legacy, and metadata-derived shell state across view, loading, error, and edit rendering.
-- [ ] 2.2 Update stylesheet regression tests to require effective shell selectors and reject the incomplete legacy host selector.
-- [ ] 2.3 Strengthen Petclinic geometry diagnostics and verify native narrow multiline alignment, bounded controls, and no overflow.
+- [x] 2.1 Add property component tests for canonical, legacy, and metadata-derived shell state across view, loading, error, and edit rendering.
+- [x] 2.2 Update stylesheet regression tests to require effective shell selectors and reject the incomplete legacy host selector.
+- [x] 2.3 Strengthen Petclinic geometry diagnostics and verify native narrow multiline alignment, bounded controls, and no overflow.
 
 ## 3. Validation and archive readiness
 
-- [ ] 3.1 Run foundation tests and package verification.
-- [ ] 3.2 Run focused and full Petclinic Playwright acceptance in default and native modes.
-- [ ] 3.3 Run Petclinic integration, strict OpenSpec validation, compilation, license, diff, and working-tree checks.
+- [x] 3.1 Run foundation tests and package verification.
+- [x] 3.2 Run focused and full Petclinic Playwright acceptance in default and native modes.
+- [x] 3.3 Run Petclinic integration, strict OpenSpec validation, compilation, license, diff, and working-tree checks.
diff --git a/viewers/webcomponents/foundation/src/property-element.mjs b/viewers/webcomponents/foundation/src/property-element.mjs
index e8b11cb50b3..cec2998ca0f 100644
--- a/viewers/webcomponents/foundation/src/property-element.mjs
+++ b/viewers/webcomponents/foundation/src/property-element.mjs
@@ -487,11 +487,11 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     }
     const presentation = this.#presentation(state);
     if (presentation.loading) {
-      renderMemberPrimary(this, `<div class="causeway-property" data-label-position="${presentation.labelPosition}" aria-busy="true">${presentation.labelMarkup}${presentation.descriptionMarkup}${presentation.disabledMarkup}<span class="causeway-property-field" role="status">Loading value…</span></div>`);
+      renderMemberPrimary(this, `<div class="causeway-property" data-label-position="${presentation.labelPosition}"${this.#multiLineAttribute(state)} aria-busy="true">${presentation.labelMarkup}${presentation.descriptionMarkup}${presentation.disabledMarkup}<span class="causeway-property-field" role="status">Loading value…</span></div>`);
       return;
     }
     if (presentation.error) {
-      renderMemberPrimary(this, `<div class="causeway-property causeway-error" data-label-position="${presentation.labelPosition}" role="alert">${presentation.labelMarkup}${presentation.descriptionMarkup}${presentation.disabledMarkup}<span class="causeway-property-field">${escapeHtml(errorMessage(state))}</span></div>`);
+      renderMemberPrimary(this, `<div class="causeway-property causeway-error" data-label-position="${presentation.labelPosition}"${this.#multiLineAttribute(state)} role="alert">${presentation.labelMarkup}${presentation.descriptionMarkup}${presentation.disabledMarkup}<span class="causeway-property-field">${escapeHtml(errorMessage(state))}</span></div>`);
       return;
     }
     const propertyState = state.data ?? {};
@@ -514,7 +514,7 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     const editMarkup = this.#canOfferEdit(state)
       ? `<button type="button" class="causeway-property-edit" data-causeway-action="edit" aria-label="${escapeHtml(editLabel)}" title="${escapeHtml(editLabel)}"${this.#testId('edit')}><svg class="causeway-property-edit-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 20h4L19 9l-4-4L4 16v4Z"></path><path d="m13.5 6.5 4 4"></path></svg></button>`
       : '';
-    renderMemberPrimary(this, `<div class="causeway-property${presentation.disabledReason ? ' causeway-disabled' : ''}" data-label-position="${presentation.labelPosition}" aria-busy="false"${presentation.disabledReason ? ' data-disabled="true"' : ''}>
+    renderMemberPrimary(this, `<div class="causeway-property${presentation.disabledReason ? ' causeway-disabled' : ''}" data-label-position="${presentation.labelPosition}"${this.#multiLineAttribute(state)} aria-busy="false"${presentation.disabledReason ? ' data-disabled="true"' : ''}>
   ${presentation.labelMarkup}
   ${presentation.disabledMarkup}
   ${presentation.descriptionMarkup}
@@ -682,6 +682,11 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
       || normalizedMultiLine(state?.data?.metadata?.multiLine);
   }
 
+  #multiLineAttribute(state) {
+    const multiLine = this.#effectiveMultiLine(state);
+    return multiLine ? ` data-multi-line="${multiLine}"` : '';
+  }
+
   #editorContext(interaction = this.interactionState) {
     const presentation = this.#presentation(this.componentState);
     return {
@@ -739,7 +744,7 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     this.setAttribute('data-editor', renderedEditor.editorId);
     this.renderingInteraction = true;
     try {
-      renderMemberPrimary(this, `<div class="causeway-property causeway-property-editing${interaction.error ? ' causeway-error' : ''}" data-label-position="${presentation.labelPosition}" aria-busy="${busy}">
+      renderMemberPrimary(this, `<div class="causeway-property causeway-property-editing${interaction.error ? ' causeway-error' : ''}" data-label-position="${presentation.labelPosition}"${this.#multiLineAttribute(state)} aria-busy="${busy}">
   ${presentation.labelMarkup}
   ${presentation.descriptionMarkup}
   <div class="causeway-property-field">
diff --git a/viewers/webcomponents/foundation/src/theme.css b/viewers/webcomponents/foundation/src/theme.css
index 2ea8f618b06..45aef3ade6b 100644
--- a/viewers/webcomponents/foundation/src/theme.css
+++ b/viewers/webcomponents/foundation/src/theme.css
@@ -368,27 +368,21 @@ cw-property:last-child .causeway-property {
   font-size: var(--causeway-font-size-sm);
 }
 
-:where(cw-property[multiline]) .causeway-property-label {
+.causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-label {
   grid-column: 1;
   grid-row: 1;
 }
 
-:where(cw-property[multiline]) .causeway-property-description {
+.causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-description {
   grid-column: 1;
   grid-row: 2;
 }
 
-:where(cw-property[multiline]) .causeway-property-value {
+.causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-field {
   grid-column: 2;
   grid-row: 1 / span 2;
 }
 
-:where(cw-property[multiline]) .causeway-property-edit {
-  grid-column: 3;
-  grid-row: 1;
-  justify-self: start;
-}
-
 .causeway-action-description {
   position: absolute !important;
   width: 1px !important;
@@ -684,20 +678,17 @@ cw-interaction-controller:has(.causeway-action-prompt)::before {
     grid-column: 1;
   }
 
-  :where(cw-property[multiline]) .causeway-property-label {
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-label {
     grid-row: 1;
   }
 
-  :where(cw-property[multiline]) .causeway-property-description {
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-description {
     grid-row: 2;
   }
 
-  :where(cw-property[multiline]) .causeway-property-value {
-    grid-row: 3;
-  }
-
-  :where(cw-property[multiline]) .causeway-property-edit {
-    grid-column: 2;
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-field {
+    align-items: flex-start;
+    grid-column: 1 / -1;
     grid-row: 3;
   }
 }
@@ -737,20 +728,17 @@ cw-interaction-controller:has(.causeway-action-prompt)::before {
     grid-column: 1;
   }
 
-  :where(cw-property[multiline]) .causeway-property-label {
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-label {
     grid-row: 1;
   }
 
-  :where(cw-property[multiline]) .causeway-property-description {
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-description {
     grid-row: 2;
   }
 
-  :where(cw-property[multiline]) .causeway-property-value {
-    grid-row: 3;
-  }
-
-  :where(cw-property[multiline]) .causeway-property-edit {
-    grid-column: 2;
+  .causeway-property[data-multi-line][data-label-position="LEFT"] .causeway-property-field {
+    align-items: flex-start;
+    grid-column: 1 / -1;
     grid-row: 3;
   }
 
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index 1bbe5cef39f..92c7b9a9816 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -85,11 +85,12 @@ test('direct member associations wrap in semantic source and keyboard order', ()
   assert.match(CAUSEWAY_COMPONENT_STYLES, /--causeway-associated-action-gap/);
 });
 
-test('standard theme gives described multiline properties explicit responsive placement', async () => {
+test('standard theme gives effective multiline properties explicit responsive placement', async () => {
   const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
-  assert.match(theme, /:where\(cw-property\[multiline\]\) \.causeway-property-description \{\s+grid-column: 1;\s+grid-row: 2;/);
-  assert.match(theme, /:where\(cw-property\[multiline\]\) \.causeway-property-value \{\s+grid-column: 2;\s+grid-row: 1 \/ span 2;/);
-  assert.match(theme, /:where\(cw-property\[multiline\]\) \.causeway-property-edit \{\s+grid-column: 3;\s+grid-row: 1;\s+justify-self: start;/);
-  assert.match(theme, /@container \(max-width: 32rem\)[\s\S]+:where\(cw-property\[multiline\]\) \.causeway-property-value \{\s+grid-row: 3;/);
-  assert.match(theme, /@media \(max-width: 48rem\)[\s\S]+:where\(cw-property\[multiline\]\) \.causeway-property-edit \{\s+grid-column: 2;\s+grid-row: 3;/);
+  const effectiveMultiline = '\\.causeway-property\\[data-multi-line\\]\\[data-label-position="LEFT"\\]';
+  assert.match(theme, new RegExp(`${effectiveMultiline} \\.causeway-property-description \\{\\s+grid-column: 1;\\s+grid-row: 2;`));
+  assert.match(theme, new RegExp(`${effectiveMultiline} \\.causeway-property-field \\{\\s+grid-column: 2;\\s+grid-row: 1 / span 2;`));
+  assert.match(theme, new RegExp(`@container \\(max-width: 32rem\\)[\\s\\S]+${effectiveMultiline} \\.causeway-property-field \\{\\s+align-items: flex-start;\\s+grid-column: 1 / -1;\\s+grid-row: 3;`));
+  assert.match(theme, new RegExp(`@media \\(max-width: 48rem\\)[\\s\\S]+${effectiveMultiline} \\.causeway-property-field \\{\\s+align-items: flex-start;\\s+grid-column: 1 / -1;\\s+grid-row: 3;`));
+  assert.doesNotMatch(theme, /cw-property\[multiline\]/);
 });
diff --git a/viewers/webcomponents/foundation/test/components.test.mjs b/viewers/webcomponents/foundation/test/components.test.mjs
index 7a4b0c7e980..83860432ab1 100644
--- a/viewers/webcomponents/foundation/test/components.test.mjs
+++ b/viewers/webcomponents/foundation/test/components.test.mjs
@@ -126,9 +126,45 @@ test('property presentation attributes override canonical metadata with bounded
   assert.match(property.innerHTML, />Given name<\/span>/);
   assert.match(property.innerHTML, /causeway-property-description[^>]*>The given or first name of this customer<\/span>/);
   assert.match(property.innerHTML, /data-rows="5"/);
+  assert.match(property.innerHTML, /data-multi-line="5"/);
   assert.doesNotMatch(property.innerHTML, /Metadata description|Compatibility name/);
 });
 
+test('effective multiline state is reflected on every rendered property shell', () => {
+  const property = new CausewayPropertyElement();
+  property.id = 'notes';
+  property.setAttribute('multi-line', '5');
+
+  property.renderComponentState(state({status: 'object-loading'}));
+  assert.match(property.innerHTML, /data-multi-line="5"/);
+  property.renderComponentState(state({status: 'partial-error', errors: [{message: 'Unavailable'}]}));
+  assert.match(property.innerHTML, /data-multi-line="5"/);
+
+  property.removeAttribute('multi-line');
+  property.setAttribute('multiline', '3');
+  property.renderComponentState(state({data: {
+    hidden: false,
+    disabled: null,
+    datatype: 'String',
+    metadata: {friendlyName: 'Notes', description: null, multiLine: 4, labelPosition: 'LEFT'},
+    get: 'Details'
+  }}));
+  assert.match(property.innerHTML, /data-multi-line="3"/);
+
+  property.removeAttribute('multiline');
+  property.renderComponentState(state({data: {
+    hidden: false,
+    disabled: null,
+    datatype: 'String',
+    metadata: {friendlyName: 'Notes', description: null, multiLine: 4, labelPosition: 'LEFT'},
+    get: 'Details'
+  }}));
+  assert.match(property.innerHTML, /data-multi-line="4"/);
+
+  property.renderComponentState(state({data: {hidden: false, disabled: null, datatype: 'String', get: 'Details'}}));
+  assert.doesNotMatch(property.innerHTML, /data-multi-line=/);
+});
+
 test('property metadata supplies descriptions and label positions while NONE suppresses visible presentation', () => {
   const property = new CausewayPropertyElement();
   property.id = 'emailAddress';
@@ -176,6 +212,7 @@ test('invalid authored presentation values fall back to metadata and compatibili
   assert.equal(property.labelPosition, '');
   assert.match(property.innerHTML, /data-label-position="TOP"/);
   assert.match(property.innerHTML, /data-rows="50"/);
+  assert.match(property.innerHTML, /data-multi-line="50"/);
 });
 
 test('qualified standard values use a read-only field while native policy preserves the standard renderer', () => {
diff --git a/viewers/webcomponents/foundation/test/interaction.test.mjs b/viewers/webcomponents/foundation/test/interaction.test.mjs
index 50cb70fda87..427894dcd02 100644
--- a/viewers/webcomponents/foundation/test/interaction.test.mjs
+++ b/viewers/webcomponents/foundation/test/interaction.test.mjs
@@ -372,6 +372,7 @@ test('editable properties support prepare, validation, cancel and authoritative
     data: {hidden: false, disabled: null, get: 'Classics'}, errors: [], generation: 3
   });
   assert.match(property.innerHTML, /property-name-edit/);
+  assert.match(property.innerHTML, /data-multi-line="5"/);
   assert.match(property.innerHTML, /causeway-property-value-string/);
   assert.doesNotMatch(property.innerHTML, /causeway-property-disabled-tooltip|data-tooltip="Locked by policy"|tabindex="0"/);
   assert.match(property.innerHTML, /class="causeway-property-edit"[^>]+data-causeway-action="edit"/);
@@ -381,6 +382,7 @@ test('editable properties support prepare, validation, cancel and authoritative
   assert.doesNotMatch(property.innerHTML, />Edit Name</);
   assert.equal(await property.beginEdit(), true);
   assert.match(property.innerHTML, /property-name-editor/);
+  assert.match(property.innerHTML, /causeway-property-editing[^>]+data-multi-line="5"/);
   assert.match(property.innerHTML, /data-control="text-area"[^>]+data-rows="5"/);
   assert.doesNotMatch(property.innerHTML, /causeway-property-interaction-status/);
   assert.doesNotMatch(property.innerHTML, />Editing</);
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 b8fee8c5d7c..2356e7e63a3 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
@@ -966,7 +966,9 @@ private void assertNarrowMultilinePropertyLayout(final Locator property) {
         final var geometry = (List<Number>) property.evaluate("element => { const label = element.querySelector('.causeway-property-label').getBoundingClientRect(); const description = element.querySelector('.causeway-property-description').getBoundingClientRect(); const value = element.querySelector('.causeway-property-value').getBoundingClientRect(); const edit = element.querySelector('.causeway-property-edit').getBoundingClientRect(); return [label.bottom, description.top, description [...]
         assertThat(geometry.get(1).doubleValue()).isGreaterThanOrEqualTo(geometry.get(0).doubleValue() - 1.0);
         assertThat(geometry.get(3).doubleValue()).isGreaterThanOrEqualTo(geometry.get(2).doubleValue() - 1.0);
-        assertThat(geometry.get(4).doubleValue()).isGreaterThanOrEqualTo(geometry.get(3).doubleValue() - 1.0);
+        assertThat(Math.abs(geometry.get(4).doubleValue() - geometry.get(3).doubleValue()))
+                .as("narrow multiline edit and value top alignment: %s", geometry)
+                .isLessThanOrEqualTo(1.0);
         assertThat(geometry.get(5).doubleValue()).isLessThan(160.0);
         assertThat(geometry.get(6).doubleValue()).isLessThanOrEqualTo(geometry.get(7).doubleValue() + 1.0);
     }


(causeway) 03/06: docs(openspec): archive collection headings

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 88203c35bf6a3532eb1a370e60f62b0071bd131b
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 15:57:21 2026 +0200

    docs(openspec): archive collection headings
---
 .../.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/graphql-web-component-context/spec.md    |  0
 .../tasks.md                                       |  0
 openspec/specs/domain-web-components/spec.md       | 41 ++++++++++++++++++++++
 .../generic-htmx-web-component-viewer/spec.md      | 26 ++++++++++++++
 .../specs/graphql-web-component-context/spec.md    | 18 ++++++++++
 10 files changed, 85 insertions(+)

diff --git a/openspec/changes/enhance-collection-headings/.openspec.yaml b/openspec/changes/archive/2026-08-29-enhance-collection-headings/.openspec.yaml
similarity index 100%
rename from openspec/changes/enhance-collection-headings/.openspec.yaml
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/.openspec.yaml
diff --git a/openspec/changes/enhance-collection-headings/design.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/design.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/design.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/design.md
diff --git a/openspec/changes/enhance-collection-headings/proposal.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/proposal.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/proposal.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/proposal.md
diff --git a/openspec/changes/enhance-collection-headings/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/domain-web-components/spec.md
diff --git a/openspec/changes/enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/generic-htmx-web-component-viewer/spec.md
diff --git a/openspec/changes/enhance-collection-headings/specs/graphql-web-component-context/spec.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/graphql-web-component-context/spec.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/specs/graphql-web-component-context/spec.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/specs/graphql-web-component-context/spec.md
diff --git a/openspec/changes/enhance-collection-headings/tasks.md b/openspec/changes/archive/2026-08-29-enhance-collection-headings/tasks.md
similarity index 100%
rename from openspec/changes/enhance-collection-headings/tasks.md
rename to openspec/changes/archive/2026-08-29-enhance-collection-headings/tasks.md
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index 10ffc1e0eb5..49110278aac 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -1644,3 +1644,44 @@ The component registry, ECMAScript exports, semantic element-name constants, sty
 #### Scenario: Application uses plain HTML
 - **WHEN** an application imports the standard foundation entry module and writes `<cw-breadcrumbs>` beneath an object context
 - **THEN** the element is registered and operates without a frontend framework or direct GraphQL document construction
+
+### Requirement: Semantic collection headings
+The `<cw-collection>` component SHALL render one semantic collection heading from an explicit HTML name, canonical member metadata, or a safe member-id fallback.
+It SHALL render a distinct available description directly below that heading in subdued, smaller text and associate both texts with every collection presentation state.
+
+#### Scenario: HTML supplies both overrides
+- **WHEN** a collection declares non-blank `named` and `described-as` attributes
+- **THEN** those values are the rendered collection name and description
+- **AND** they override canonical friendly-name and description metadata
+
+#### Scenario: Canonical metadata supplies heading text
+- **WHEN** no applicable HTML override exists and the collection response supplies canonical `metadata.friendlyName` or `metadata.description`
+- **THEN** each available value is used independently
+- **AND** a missing value falls back to a humanized member id for the name or no description for the description
+
+#### Scenario: Legacy label remains present
+- **WHEN** a collection declares the existing `label` attribute without `named`
+- **THEN** the label remains the effective name for backward compatibility
+- **AND** `named` takes precedence when both are non-blank
+
+#### Scenario: Description duplicates resolved name
+- **WHEN** the candidate description equals the resolved name after trimming and case-insensitive comparison
+- **THEN** no duplicate description is rendered or referenced
+
+#### Scenario: Heading attributes change
+- **WHEN** `named`, `described-as`, `label`, or the member id changes while the component is connected
+- **THEN** the current collection shell rerenders with the newly resolved accessible heading
+- **AND** collection data is not independently reloaded solely for the text change
+
+### Requirement: Quiet read-only collection presentation
+Collection wrappers SHALL remain read-only and authorization-aware without rendering their disabled or unmodifiable boolean or reason as visible explanatory text, a label, or a tooltip.
+
+#### Scenario: Mixed-in collection is unmodifiable
+- **WHEN** a readable mixed-in collection reports a disabled reason such as “Cannot edit a mixed-in collection”
+- **THEN** its rows, heading, description, paging, and applicable associated actions remain available
+- **AND** the collection-level reason is not rendered as text or tooltip
+
+#### Scenario: Member or associated action is disabled
+- **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 suppression does not broaden authorization or enable mutation
diff --git a/openspec/specs/generic-htmx-web-component-viewer/spec.md b/openspec/specs/generic-htmx-web-component-viewer/spec.md
index 4a39c5489d8..8a9d89ed0b4 100644
--- a/openspec/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/specs/generic-htmx-web-component-viewer/spec.md
@@ -653,3 +653,29 @@ The application MUST NOT require a shared foundation, HTMX viewer, or web-compon
 - **WHEN** the same application is rendered below its responsive breakpoints
 - **THEN** existing gutters, stacking, contained collection presentation, and no-overflow behavior remain effective
 - **AND** the full-width override does not introduce horizontal document overflow
+
+### Requirement: Petclinic collection heading demonstrations
+The Petclinic HTMX application SHALL selectively demonstrate canonical collection descriptions, HTML-authored collection names, and HTML-authored collection description overrides without requiring application-specific component code.
+
+#### Scenario: Domain facet supplies a description
+- **WHEN** a Petclinic collection annotated with `@CollectionLayout(describedAs)` is rendered without an HTML description override
+- **THEN** its canonical description appears below the effective collection name
+- **AND** collections without a description remain free of placeholder prose
+
+#### Scenario: HTML supplies collection heading overrides
+- **WHEN** a Petclinic resource page declares `named` or `described-as` on selected `<cw-collection>` elements
+- **THEN** those explicit values appear for those collections only
+- **AND** the same component continues using canonical metadata or fallbacks for other collections
+
+#### Scenario: Petclinic owner visits are rendered
+- **WHEN** the mixed-in owner visits collection loads
+- **THEN** its selected name and description are visible and accessible
+- **AND** “Cannot edit a mixed-in collection” or an equivalent collection-level tooltip is absent
+
+### Requirement: Petclinic collection heading regression coverage
+Petclinic integration and browser acceptance SHALL verify canonical and HTML-authored collection heading combinations, quiet read-only collection presentation, and unaffected collection navigation and actions.
+
+#### Scenario: Petclinic heading combinations are exercised
+- **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
diff --git a/openspec/specs/graphql-web-component-context/spec.md b/openspec/specs/graphql-web-component-context/spec.md
index b8d35e63194..f76b094a934 100644
--- a/openspec/specs/graphql-web-component-context/spec.md
+++ b/openspec/specs/graphql-web-component-context/spec.md
@@ -233,3 +233,21 @@ The object context SHALL associate a GraphQL error on navigable breadcrumb metad
 - **WHEN** the object response contains current metadata and member data plus an error at the breadcrumb field path
 - **THEN** the breadcrumbs consumer receives a partial-error state
 - **AND** successful header, property, action, and collection consumers retain their normal state
+
+### Requirement: Targeted collection heading metadata
+A semantic collection requirement SHALL select supported canonical `friendlyName` and `description` fields from the collection wrapper's existing metadata object alongside collection visibility and usability state.
+
+#### Scenario: Collection metadata fields are supported
+- **WHEN** targeted introspection finds `metadata.friendlyName` or `metadata.description` on a requested collection wrapper
+- **THEN** the object-context selection includes only the supported heading metadata fields
+- **AND** accepted response metadata is delivered with the current collection requirement state
+
+#### Scenario: Collection metadata is unavailable or partially erroneous
+- **WHEN** the wrapper lacks either heading field or GraphQL reports a nullable field-local error
+- **THEN** the remaining supported collection state stays usable
+- **AND** the component can apply explicit HTML overrides or safe fallbacks without a complete-schema read
+
+#### Scenario: Collection heading attribute changes
+- **WHEN** an HTML-only name or description override changes after metadata has loaded
+- **THEN** the component reuses the current requirement state
+- **AND** no extra GraphQL request is issued solely for presentation text


(causeway) 06/06: docs(openspec): archive narrow multiline layout fix

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 1dc8d3ef3d46e4bc38de4a2f0d1df2572effb77d
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Aug 29 16:20:50 2026 +0200

    docs(openspec): archive narrow multiline layout fix
---
 .../.openspec.yaml                                                | 0
 .../2026-08-29-fix-narrow-multiline-property-layout}/design.md    | 0
 .../2026-08-29-fix-narrow-multiline-property-layout}/proposal.md  | 0
 .../specs/domain-web-components/spec.md                           | 0
 .../2026-08-29-fix-narrow-multiline-property-layout}/tasks.md     | 0
 openspec/specs/domain-web-components/spec.md                      | 8 +++++---
 6 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/openspec/changes/fix-narrow-multiline-property-layout/.openspec.yaml b/openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/.openspec.yaml
similarity index 100%
rename from openspec/changes/fix-narrow-multiline-property-layout/.openspec.yaml
rename to openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/.openspec.yaml
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/design.md b/openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/design.md
similarity index 100%
rename from openspec/changes/fix-narrow-multiline-property-layout/design.md
rename to openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/design.md
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/proposal.md b/openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/proposal.md
similarity index 100%
rename from openspec/changes/fix-narrow-multiline-property-layout/proposal.md
rename to openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/proposal.md
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/specs/domain-web-components/spec.md
diff --git a/openspec/changes/fix-narrow-multiline-property-layout/tasks.md b/openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/tasks.md
similarity index 100%
rename from openspec/changes/fix-narrow-multiline-property-layout/tasks.md
rename to openspec/changes/archive/2026-08-29-fix-narrow-multiline-property-layout/tasks.md
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index 49110278aac..6063c06accd 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -1532,6 +1532,7 @@ Canonical authored attributes MUST take precedence over compatibility aliases, w
 ### Requirement: Property label-position presentation
 `<cw-property>` SHALL honour the effective property label position `LEFT`, `TOP`, or `NONE` in view, loading, error, disabled, and edit states.
 Properties using `LEFT` in the same field-set-like container MUST use a consistent configurable label-to-field ratio.
+Effective multiline presentation from canonical HTML, compatibility HTML, or member metadata MUST drive the same explicit responsive shell layout.
 
 #### Scenario: Label is positioned left
 - **WHEN** the effective label position is `LEFT`
@@ -1557,9 +1558,10 @@ Properties using `LEFT` in the same field-set-like container MUST use a consiste
 - **AND** the value or editor is associated with it through accessible description semantics
 
 #### Scenario: Narrow presentation stacks safely
-- **WHEN** a `LEFT` property cannot retain its configured columns at a narrow inline size
-- **THEN** label, description, field, and controls stack in meaningful document order
-- **AND** the property introduces no horizontal overflow, clipping, or overlap
+- **WHEN** a multiline `LEFT` property resolved from `multi-line`, legacy `multiline`, or `metadata.multiLine` cannot retain its configured columns at a narrow inline size
+- **THEN** label and description occupy explicit successive rows followed by an explicit full-width field row in meaningful document order
+- **AND** the field's value and bounded edit control align at the start of that row without overlap or implicit-grid displacement
+- **AND** the property introduces no horizontal overflow or clipping
 
 ### Requirement: Generated property presentation equivalence
 `<cw-object>` SHALL carry supported effective-grid property name, description, multiline, and label-position hints into generated `<cw-property>` elements through the canonical public attributes.