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/30 17:42:45 UTC
(causeway) branch htmx updated (320fed0244b -> e0ea7ae5173)
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 320fed0244b docs(openspec): archive action button layout improvements
new 1ee80212210 fix(webcomponents): refine action layout spacing
new 55809412530 docs(openspec): plan action parameter component
new 118ecf1baa6 feat(webcomponents): add action parameter presentation
new 6a76d6bb787 docs(openspec): sync action parameter specifications
new e0ea7ae5173 docs(openspec): archive action parameter component
The 5 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 | 86 +++++++++++++
.../proposal.md | 32 +++++
.../specs/domain-web-components/spec.md | 56 ++++++++
.../generic-htmx-web-component-viewer/spec.md | 19 +++
.../tasks.md | 23 ++++
openspec/specs/domain-web-components/spec.md | 55 ++++++++
.../generic-htmx-web-component-viewer/spec.md | 18 +++
viewers/webcomponents/foundation/USAGE.adoc | 17 +++
.../foundation/src/action-element.mjs | 51 +++++++-
.../foundation/src/component-contracts.mjs | 4 +
.../foundation/src/component-styles.css | 6 +
.../foundation/src/component-styles.mjs | 6 +
viewers/webcomponents/foundation/src/index.mjs | 1 +
.../src/interaction-controller-element.mjs | 51 ++++++--
.../foundation/src/parameter-element.mjs | 141 +++++++++++++++++++++
viewers/webcomponents/foundation/src/register.mjs | 5 +-
.../foundation/test/component-styles.test.mjs | 7 +-
.../foundation/test/components.test.mjs | 61 +++++++++
.../foundation/test/domain-components.test.mjs | 1 +
.../foundation/test/element-prefix.test.mjs | 6 +-
.../foundation/test/interaction.test.mjs | 32 +++++
.../webcomponents/pages/petclinic.PetOwner.html | 22 +++-
.../src/main/resources/static/css/application.css | 2 +-
.../petclinic/PetClinicHtmxPlaywrightTest.java | 18 +++
.../PetClinicHtmxApplication_IntegTest.java | 11 +-
26 files changed, 709 insertions(+), 22 deletions(-)
copy openspec/changes/archive/{2026-08-30-enhance-action-presentation => 2026-08-30-add-action-parameter-component}/.openspec.yaml (100%)
create mode 100644 openspec/changes/archive/2026-08-30-add-action-parameter-component/design.md
create mode 100644 openspec/changes/archive/2026-08-30-add-action-parameter-component/proposal.md
create mode 100644 openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/domain-web-components/spec.md
create mode 100644 openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
create mode 100644 openspec/changes/archive/2026-08-30-add-action-parameter-component/tasks.md
create mode 100644 viewers/webcomponents/foundation/src/parameter-element.mjs
(causeway) 03/05: feat(webcomponents): add action parameter presentation
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 118ecf1baa6bc4ddd0912ae5f09cb0a4a1de9ce3
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 19:35:17 2026 +0200
feat(webcomponents): add action parameter presentation
---
.../add-action-parameter-component/tasks.md | 24 ++--
viewers/webcomponents/foundation/USAGE.adoc | 17 +++
.../foundation/src/action-element.mjs | 51 +++++++-
.../foundation/src/component-contracts.mjs | 4 +
.../foundation/src/component-styles.css | 3 +
.../foundation/src/component-styles.mjs | 3 +
viewers/webcomponents/foundation/src/index.mjs | 1 +
.../src/interaction-controller-element.mjs | 51 ++++++--
.../foundation/src/parameter-element.mjs | 141 +++++++++++++++++++++
viewers/webcomponents/foundation/src/register.mjs | 5 +-
.../foundation/test/component-styles.test.mjs | 6 +-
.../foundation/test/components.test.mjs | 61 +++++++++
.../foundation/test/domain-components.test.mjs | 1 +
.../foundation/test/element-prefix.test.mjs | 6 +-
.../foundation/test/interaction.test.mjs | 32 +++++
.../webcomponents/pages/petclinic.PetOwner.html | 22 +++-
.../petclinic/PetClinicHtmxPlaywrightTest.java | 18 +++
.../PetClinicHtmxApplication_IntegTest.java | 10 +-
18 files changed, 423 insertions(+), 33 deletions(-)
diff --git a/openspec/changes/add-action-parameter-component/tasks.md b/openspec/changes/add-action-parameter-component/tasks.md
index aefd86dac4e..8ff50f93825 100644
--- a/openspec/changes/add-action-parameter-component/tasks.md
+++ b/openspec/changes/add-action-parameter-component/tasks.md
@@ -1,23 +1,23 @@
## 1. Public parameter component
-- [ ] 1.1 Add the `<cw-parameter>` element with normalized immutable `named`, `described-as`, `description-as`, and bounded `multi-line` configuration.
-- [ ] 1.2 Register and export the element and extend public element, host, attribute, and configuration-event contracts.
-- [ ] 1.3 Add synchronized structural styling and public usage documentation for the non-visual nested configuration component.
+- [x] 1.1 Add the `<cw-parameter>` element with normalized immutable `named`, `described-as`, `description-as`, and bounded `multi-line` configuration.
+- [x] 1.2 Register and export the element and extend public element, host, attribute, and configuration-event contracts.
+- [x] 1.3 Add synchronized structural styling and public usage documentation for the non-visual nested configuration component.
## 2. Action and prompt integration
-- [ ] 2.1 Capture initial and dynamic parameter declarations in `<cw-action>` without making them authoritative action parameters.
-- [ ] 2.2 Carry immutable parameter presentation hints through semantic action requests into standard prompt state.
-- [ ] 2.3 Merge matching names, descriptions, label-or-tooltip presentation, and multiline rows into parameter markup and editor context while preserving canonical interaction state.
-- [ ] 2.4 Add component and interaction tests for partial declarations, unknown ids, fallback behavior, accessibility, dynamic updates, and multiline qualification.
+- [x] 2.1 Capture initial and dynamic parameter declarations in `<cw-action>` without making them authoritative action parameters.
+- [x] 2.2 Carry immutable parameter presentation hints through semantic action requests into standard prompt state.
+- [x] 2.3 Merge matching names, descriptions, label-or-tooltip presentation, and multiline rows into parameter markup and editor context while preserving canonical interaction state.
+- [x] 2.4 Add component and interaction tests for partial declarations, unknown ids, fallback behavior, accessibility, dynamic updates, and multiline qualification.
## 3. Petclinic demonstration
-- [ ] 3.1 Add representative partial `<cw-parameter>` declarations to selected Petclinic parameterized actions while leaving other parameters and actions undeclared.
-- [ ] 3.2 Extend Petclinic resource, integration, and applicable browser assertions for authored and canonical parameter presentation.
+- [x] 3.1 Add representative partial `<cw-parameter>` declarations to selected Petclinic parameterized actions while leaving other parameters and actions undeclared.
+- [x] 3.2 Extend Petclinic resource, integration, and applicable browser assertions for authored and canonical parameter presentation.
## 4. Verification
-- [ ] 4.1 Run the complete foundation test suite.
-- [ ] 4.2 Run the applicable Petclinic Maven tests.
-- [ ] 4.3 Run strict OpenSpec validation and verify synchronized installable styles.
+- [x] 4.1 Run the complete foundation test suite.
+- [x] 4.2 Run the applicable Petclinic Maven tests.
+- [x] 4.3 Run strict OpenSpec validation and verify synchronized installable styles.
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index 6c123c2dc94..a23f1d86b09 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -425,6 +425,23 @@ The prompt resolves parameters in schema order and consumes advertised hidden, d
Changing a parameter recomputes dependent later semantics using the current pending values.
Escape cancels an open prompt, and prompt completion restores focus to the originating action.
+An authored action can optionally nest `<cw-parameter>` presentation hints for selected parameters.
+A declaration matches only an authoritative parameter with the same `id`; omitted declarations and omitted attributes retain canonical behavior.
+`named` overrides the humanized parameter name, `described-as` overrides its canonical description, and `description-as="label|tooltip"` selects visible or tooltip presentation with `label` as the default.
+A valid `multi-line` value greater than one requests the established bounded multiline string editor without changing parsing, validation, or invocation.
+Declarations never add, remove, reorder, hide, disable, default, validate, or invoke parameters.
+
+[source,html]
+----
+<cw-action id="bookVisit">
+ <cw-parameter id="reason"
+ named="Reason for visit"
+ described-as="Brief clinical reason for the appointment"
+ description-as="tooltip"
+ multi-line="3"></cw-parameter>
+</cw-action>
+----
+
An application claims an action by calling `preventDefault()` on `causeway-action-request`; the standard controller then does nothing:
[source,javascript]
diff --git a/viewers/webcomponents/foundation/src/action-element.mjs b/viewers/webcomponents/foundation/src/action-element.mjs
index e3664e25180..75b4576aeab 100644
--- a/viewers/webcomponents/foundation/src/action-element.mjs
+++ b/viewers/webcomponents/foundation/src/action-element.mjs
@@ -30,9 +30,27 @@ import {
import {CausewaySemanticEvent} from './component-contracts.mjs';
import {CausewayContextConsumerElement} from './context-consumer-element.mjs';
import {createSemanticEvent} from './context-events.mjs';
+import {
+ actionParameterConfiguration,
+ normalizeActionParameterConfigurations
+} from './parameter-element.mjs';
import {errorMessage, escapeHtml} from './rendering.mjs';
let actionSequence = 0;
+const initialParameterConfigurations = new WeakMap();
+
+export function captureDeclarativeActionParameters(root = globalThis.document) {
+ if (!root?.querySelectorAll) return;
+ const actions = root.localName === 'cw-action' ? [root] : root.querySelectorAll('cw-action');
+ for (const action of actions) {
+ const configurations = [...(action.children ?? action.childNodes ?? [])]
+ .filter(child => child?.localName === 'cw-parameter' || child?.configuration?.parameter)
+ .map(child => child?.configuration ?? actionParameterConfiguration(child));
+ if (configurations.length > 0) {
+ initialParameterConfigurations.set(action, normalizeActionParameterConfigurations(configurations));
+ }
+ }
+}
export class CausewayActionElement extends CausewayContextConsumerElement {
static get observedAttributes() {
@@ -44,6 +62,12 @@ export class CausewayActionElement extends CausewayContextConsumerElement {
const sequence = ++actionSequence;
this.descriptionId = `causeway-action-description-${sequence}`;
this.reasonId = `causeway-action-reason-${sequence}`;
+ this._parameterPresentations = [];
+ this.addEventListener(CausewaySemanticEvent.ACTION_PARAMETER_CONFIGURATION, event => {
+ if (event.target?.parentNode !== this) return;
+ event.stopPropagation();
+ this.#acceptParameterPresentation(event.detail?.parameter);
+ });
this.addEventListener('click', event => {
if (originatesFromOrdinaryActionControl(this, event.target)) {
this.activate();
@@ -60,6 +84,8 @@ export class CausewayActionElement extends CausewayContextConsumerElement {
}
connectedCallback() {
+ captureDeclarativeActionParameters(this);
+ this.#captureParameterPresentations();
document.addEventListener(CAUSEWAY_ACTION_WIDGET_POLICY_EVENT, this._actionWidgetPolicyListener);
super.connectedCallback();
}
@@ -78,6 +104,10 @@ export class CausewayActionElement extends CausewayContextConsumerElement {
else this.setAttribute('named', value);
}
+ get parameterPresentations() {
+ return normalizeActionParameterConfigurations(this._parameterPresentations);
+ }
+
get label() {
return this.getAttribute('label') || '';
}
@@ -179,12 +209,31 @@ export class CausewayActionElement extends CausewayContextConsumerElement {
actionPresentation(state = this.componentState) {
const metadata = state?.data?.metadata ?? {};
- return normalizeActionPresentation({
+ const presentation = normalizeActionPresentation({
name: this.named || this.label || metadata.friendlyName || humanize(this.id),
description: metadata.description || state?.descriptor?.description || '',
cssClassFa: metadata.cssClassFa,
cssClassFaPosition: metadata.cssClassFaPosition
});
+ const parameters = this.parameterPresentations;
+ return parameters.length > 0 ? Object.freeze({...presentation, parameters}) : presentation;
+ }
+
+ #acceptParameterPresentation(configuration) {
+ const parameters = normalizeActionParameterConfigurations([...this._parameterPresentations, configuration]);
+ this._parameterPresentations = [...parameters];
+ }
+
+ #captureParameterPresentations() {
+ for (const configuration of initialParameterConfigurations.get(this) ?? []) {
+ this.#acceptParameterPresentation(configuration);
+ }
+ initialParameterConfigurations.delete(this);
+ for (const child of this.childNodes ?? []) {
+ if (child?.localName === 'cw-parameter') {
+ this.#acceptParameterPresentation(child?.configuration ?? actionParameterConfiguration(child));
+ }
+ }
}
}
diff --git a/viewers/webcomponents/foundation/src/component-contracts.mjs b/viewers/webcomponents/foundation/src/component-contracts.mjs
index 65ea57e1593..c747539c857 100644
--- a/viewers/webcomponents/foundation/src/component-contracts.mjs
+++ b/viewers/webcomponents/foundation/src/component-contracts.mjs
@@ -30,6 +30,7 @@ export const CausewayElementName = Object.freeze({
VALUE: 'cw-value',
OBJECT_LINK: 'cw-object-link',
ACTION: 'cw-action',
+ PARAMETER: 'cw-parameter',
INTERACTION_CONTROLLER: 'cw-interaction-controller',
REFERENCE_EDITOR: 'cw-reference-editor',
COLLECTION: 'cw-collection',
@@ -51,6 +52,7 @@ export const CausewayHostClass = Object.freeze({
VALUE: 'causeway-value',
OBJECT_LINK: 'causeway-object-link',
ACTION: 'causeway-action',
+ PARAMETER: 'causeway-parameter',
INTERACTION_CONTROLLER: 'causeway-interaction-controller',
ACTION_PROMPT: 'causeway-action-prompt',
ACTION_RESULT: 'causeway-action-result',
@@ -77,6 +79,7 @@ export const CausewayAttribute = Object.freeze({
LABEL: 'label',
NAMED: 'named',
DESCRIBED_AS: 'described-as',
+ DESCRIPTION_AS: 'description-as',
MULTI_LINE: 'multi-line',
LABEL_POSITION: 'label-position',
ACTIVE: 'active',
@@ -97,6 +100,7 @@ export const CausewayAttribute = Object.freeze({
export const CausewaySemanticEvent = Object.freeze({
NAVIGATION_REQUEST: 'causeway-navigation-request',
ACTION_REQUEST: 'causeway-action-request',
+ ACTION_PARAMETER_CONFIGURATION: 'causeway-action-parameter-configuration-change',
ACTION_PROMPT_STATE: 'causeway-action-prompt-state-change',
ACTION_RESULT: 'causeway-action-result',
PROPERTY_INTERACTION_STATE: 'causeway-property-interaction-state-change',
diff --git a/viewers/webcomponents/foundation/src/component-styles.css b/viewers/webcomponents/foundation/src/component-styles.css
index 37cf9064537..d5179b68f8a 100644
--- a/viewers/webcomponents/foundation/src/component-styles.css
+++ b/viewers/webcomponents/foundation/src/component-styles.css
@@ -17,6 +17,9 @@
* under the License.
*/
+cw-parameter {
+ display: none;
+}
cw-object {
container-name: cw-object;
container-type: inline-size;
diff --git a/viewers/webcomponents/foundation/src/component-styles.mjs b/viewers/webcomponents/foundation/src/component-styles.mjs
index e3cbe70904e..54ecb5c874a 100644
--- a/viewers/webcomponents/foundation/src/component-styles.mjs
+++ b/viewers/webcomponents/foundation/src/component-styles.mjs
@@ -18,6 +18,9 @@
*/
export const CAUSEWAY_COMPONENT_STYLES = `
+cw-parameter {
+ display: none;
+}
cw-object {
container-name: cw-object;
container-type: inline-size;
diff --git a/viewers/webcomponents/foundation/src/index.mjs b/viewers/webcomponents/foundation/src/index.mjs
index ab77f981441..720f3bdd8eb 100644
--- a/viewers/webcomponents/foundation/src/index.mjs
+++ b/viewers/webcomponents/foundation/src/index.mjs
@@ -51,6 +51,7 @@ export * from './object-element.mjs';
export * from './object-header-element.mjs';
export * from './object-layout.mjs';
export * from './object-link-element.mjs';
+export * from './parameter-element.mjs';
export * from './property-element.mjs';
export * from './reference-widget.mjs';
export * from './register.mjs';
diff --git a/viewers/webcomponents/foundation/src/interaction-controller-element.mjs b/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
index bbbb7f09a63..67942b2eb40 100644
--- a/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
+++ b/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
@@ -20,9 +20,14 @@
import {normalizeActionPresentation} from './action-presentation.mjs';
import {CausewaySemanticEvent} from './component-contracts.mjs';
import {createSemanticEvent} from './context-events.mjs';
+import {
+ boundedTooltipSection,
+ DescriptionPresentation
+} from './description-presentation.mjs';
import {defaultEditorRegistry, parseCausewayEditorValue, renderCausewayEditor} from './editor-registry.mjs';
import {causewayReferenceWidgetConfiguration} from './reference-widget.mjs';
import {escapeHtml} from './rendering.mjs';
+import {normalizeActionParameterConfigurations} from './parameter-element.mjs';
import {InteractionStatus} from './types.mjs';
import {CausewayValueCodecError, semanticTypeName} from './value-codecs.mjs';
@@ -182,6 +187,7 @@ export class CausewayInteractionControllerElement extends HTMLElement {
cssClassFa: presentation?.icon?.classes?.join(' '),
cssClassFaPosition: presentation?.icon?.position
}),
+ parameterPresentations: normalizeActionParameterConfigurations(presentation?.parameters),
values: Object.freeze({}),
parameters: Object.freeze([]),
error: null
@@ -646,14 +652,21 @@ export class CausewayInteractionControllerElement extends HTMLElement {
return '';
}
const choices = parameter.state?.choices ?? parameter.enumValues ?? [];
- const editorContext = this.#parameterEditorContext(parameter, choices);
+ const presentation = this.#parameterPresentation(parameter);
+ const editorContext = this.#parameterEditorContext(parameter, choices, presentation);
const rendered = renderCausewayEditor(editorContext, this._editorRegistry);
- const label = humanize(parameter.id);
- const description = this.#parameterDescription(parameter, label);
const reason = protectedPromptText(this.promptState, this.#parameterReason(parameter));
- return `<div class="causeway-action-parameter${reason ? ' causeway-error' : ''}" data-parameter="${escapeHtml(parameter.id)}">
- <label id="${editorContext.labelId}" for="${editorContext.inputId}">${escapeHtml(label)}</label>
- ${description ? `<span id="${editorContext.descriptionId}" class="causeway-action-parameter-description">${escapeHtml(description)}</span>` : ''}
+ const tooltip = presentation.descriptionAs === DescriptionPresentation.TOOLTIP
+ ? boundedTooltipSection(presentation.description)
+ : '';
+ const labelClass = `causeway-action-parameter-label${tooltip ? ' causeway-member-tooltip' : ''}`;
+ const labelAttributes = tooltip
+ ? ` tabindex="0" data-tooltip="${escapeHtml(tooltip)}" aria-describedby="${editorContext.descriptionId}"`
+ : '';
+ const descriptionClass = `causeway-action-parameter-description${presentation.descriptionAs === DescriptionPresentation.TOOLTIP ? ' causeway-visually-hidden' : ''}`;
+ return `<div class="causeway-action-parameter${reason ? ' causeway-error' : ''}" data-parameter="${escapeHtml(parameter.id)}"${presentation.multiLine ? ` data-multi-line="${presentation.multiLine}"` : ''}>
+ <label id="${editorContext.labelId}" class="${labelClass}" for="${editorContext.inputId}"${labelAttributes}>${escapeHtml(presentation.label)}</label>
+ ${presentation.description ? `<span id="${editorContext.descriptionId}" class="${descriptionClass}">${escapeHtml(presentation.description)}</span>` : ''}
${rendered.html}
${reason ? `<span id="${editorContext.errorId}" class="causeway-action-parameter-reason" role="alert">${escapeHtml(reason)}</span>` : ''}
</div>`;
@@ -666,12 +679,12 @@ export class CausewayInteractionControllerElement extends HTMLElement {
return validationReason || parameter.state?.disabled || '';
}
- #parameterEditorContext(parameter, choices) {
+ #parameterEditorContext(parameter, choices, presentation = this.#parameterPresentation(parameter)) {
const state = this.promptState;
const reason = this.#parameterReason(parameter);
return {
name: parameter.id,
- label: humanize(parameter.id),
+ label: presentation.label,
value: Object.prototype.hasOwnProperty.call(state.values, parameter.id)
? state.values[parameter.id]
: parameter.state?.default ?? null,
@@ -685,18 +698,32 @@ export class CausewayInteractionControllerElement extends HTMLElement {
inputType: parameter.inputType,
semanticType: parameter.state?.datatype ?? null,
required: parameter.inputType?.kind === 'NON_NULL',
+ multiLine: presentation.multiLine,
inputId: `causeway-action-parameter-${parameter.id}`,
labelId: `causeway-action-parameter-${parameter.id}-label`,
- descriptionId: this.#parameterDescription(parameter) ? `causeway-action-parameter-${parameter.id}-description` : '',
+ descriptionId: presentation.description ? `causeway-action-parameter-${parameter.id}-description` : '',
errorId: reason ? `causeway-action-parameter-${parameter.id}-error` : '',
testId: `action-prompt-parameter-${parameter.id}`,
disabled: Boolean(parameter.state?.disabled) || state.status === InteractionStatus.INVOKING
};
}
- #parameterDescription(parameter, label = humanize(parameter.id)) {
- const description = String(parameter.description ?? '').trim();
- return description.toLocaleLowerCase() === label.trim().toLocaleLowerCase() ? '' : description;
+ #parameterPresentation(parameter) {
+ const authored = this.promptState?.parameterPresentations
+ ?.find(candidate => candidate.parameter === parameter.id);
+ const label = authored && authored.named !== null ? authored.named : humanize(parameter.id);
+ const candidateDescription = authored && authored.describedAs !== null
+ ? authored.describedAs
+ : String(parameter.description ?? '');
+ const description = candidateDescription.trim().toLocaleLowerCase() === label.trim().toLocaleLowerCase()
+ ? ''
+ : candidateDescription;
+ return Object.freeze({
+ label,
+ description,
+ descriptionAs: authored?.descriptionAs ?? DescriptionPresentation.LABEL,
+ multiLine: authored?.multiLine ?? null
+ });
}
#resultMarkup() {
diff --git a/viewers/webcomponents/foundation/src/parameter-element.mjs b/viewers/webcomponents/foundation/src/parameter-element.mjs
new file mode 100644
index 00000000000..dfb57a84fec
--- /dev/null
+++ b/viewers/webcomponents/foundation/src/parameter-element.mjs
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {CausewaySemanticEvent} from './component-contracts.mjs';
+import {createSemanticEvent} from './context-events.mjs';
+import {normalizeDescriptionPresentation} from './description-presentation.mjs';
+
+const HTMLElementBase = globalThis.HTMLElement ?? class extends EventTarget {};
+const MAX_PARAMETER_ID = 256;
+const MAX_PARAMETER_NAME = 512;
+const MAX_PARAMETER_DESCRIPTION = 2_048;
+const MAX_MULTI_LINE = 50;
+
+export class CausewayParameterElement extends HTMLElementBase {
+ static get observedAttributes() {
+ return ['id', 'named', 'described-as', 'description-as', 'multi-line'];
+ }
+
+ get named() {
+ return this.getAttribute('named') || '';
+ }
+
+ set named(value) {
+ setOptionalAttribute(this, 'named', value);
+ }
+
+ get describedAs() {
+ return this.getAttribute('described-as') || '';
+ }
+
+ set describedAs(value) {
+ setOptionalAttribute(this, 'described-as', value);
+ }
+
+ get descriptionAs() {
+ return normalizeDescriptionPresentation(this.getAttribute('description-as'));
+ }
+
+ set descriptionAs(value) {
+ setOptionalAttribute(this, 'description-as', value);
+ }
+
+ get multiLine() {
+ return normalizedMultiLine(this.getAttribute('multi-line')) ?? 0;
+ }
+
+ set multiLine(value) {
+ const rows = normalizedMultiLine(value);
+ if (rows) this.setAttribute('multi-line', String(rows));
+ else this.removeAttribute('multi-line');
+ }
+
+ get configuration() {
+ return actionParameterConfiguration(this);
+ }
+
+ connectedCallback() {
+ this.hidden = true;
+ this.#notify();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue !== newValue && this.isConnected) this.#notify();
+ }
+
+ #notify() {
+ this.dispatchEvent(createSemanticEvent(
+ CausewaySemanticEvent.ACTION_PARAMETER_CONFIGURATION,
+ {parameter: this.configuration}
+ ));
+ }
+}
+
+export function actionParameterConfiguration(element) {
+ return normalizeActionParameterConfiguration({
+ parameter: element?.id,
+ named: authoredAttribute(element, 'named'),
+ describedAs: authoredAttribute(element, 'described-as'),
+ descriptionAs: authoredAttribute(element, 'description-as'),
+ multiLine: authoredAttribute(element, 'multi-line')
+ });
+}
+
+export function normalizeActionParameterConfiguration(value = {}) {
+ const parameter = boundedText(value.parameter ?? value.id, MAX_PARAMETER_ID).trim();
+ const named = optionalBoundedText(value.named, MAX_PARAMETER_NAME);
+ const describedAs = optionalBoundedText(value.describedAs, MAX_PARAMETER_DESCRIPTION);
+ const descriptionAs = value.descriptionAs == null
+ ? null
+ : normalizeDescriptionPresentation(value.descriptionAs);
+ const multiLine = normalizedMultiLine(value.multiLine);
+ return Object.freeze({parameter, named, describedAs, descriptionAs, multiLine});
+}
+
+export function normalizeActionParameterConfigurations(values = []) {
+ const byParameter = new Map();
+ for (const value of values ?? []) {
+ const configuration = normalizeActionParameterConfiguration(value);
+ if (configuration.parameter) byParameter.set(configuration.parameter, configuration);
+ }
+ return Object.freeze([...byParameter.values()]);
+}
+
+function authoredAttribute(element, name) {
+ return element?.hasAttribute?.(name) ? element.getAttribute(name) : null;
+}
+
+function normalizedMultiLine(value) {
+ if (value == null || value === '') return null;
+ const rows = Number(value);
+ return Number.isSafeInteger(rows) && rows > 1 ? Math.min(rows, MAX_MULTI_LINE) : null;
+}
+
+function optionalBoundedText(value, maximum) {
+ return value == null ? null : boundedText(value, maximum);
+}
+
+function boundedText(value, maximum) {
+ return String(value ?? '').replace(/[\u0000-\u001f\u007f]/g, '').slice(0, maximum);
+}
+
+function setOptionalAttribute(element, name, value) {
+ if (value == null) element.removeAttribute(name);
+ else element.setAttribute(name, String(value));
+}
diff --git a/viewers/webcomponents/foundation/src/register.mjs b/viewers/webcomponents/foundation/src/register.mjs
index b783a0a17ef..fdf2f602ea8 100644
--- a/viewers/webcomponents/foundation/src/register.mjs
+++ b/viewers/webcomponents/foundation/src/register.mjs
@@ -17,7 +17,7 @@
* under the License.
*/
-import {CausewayActionElement} from './action-element.mjs';
+import {captureDeclarativeActionParameters, CausewayActionElement} from './action-element.mjs';
import {CausewayBreadcrumbsElement} from './breadcrumbs-element.mjs';
import {CAUSEWAY_ACTION_CONTROL, CausewayActionControlElement} from './action-widget.mjs';
import {CausewayCollectionColumnElement} from './collection-column-element.mjs';
@@ -38,6 +38,7 @@ import {CausewayObjectContextElement} from './object-context-element.mjs';
import {CausewayObjectElement} from './object-element.mjs';
import {CausewayObjectHeaderElement} from './object-header-element.mjs';
import {CausewayObjectLinkElement} from './object-link-element.mjs';
+import {CausewayParameterElement} from './parameter-element.mjs';
import {CausewayPropertyElement} from './property-element.mjs';
import {CausewayReferenceEditorElement} from './reference-widget.mjs';
import {CausewayValueElement} from './value-element.mjs';
@@ -52,6 +53,7 @@ const DEFINITIONS = Object.freeze([
[CausewayElementName.VALUE, CausewayValueElement],
[CausewayElementName.OBJECT_LINK, CausewayObjectLinkElement],
[CausewayElementName.ACTION, CausewayActionElement],
+ [CausewayElementName.PARAMETER, CausewayParameterElement],
[CAUSEWAY_ACTION_CONTROL, CausewayActionControlElement],
[CausewayElementName.INTERACTION_CONTROLLER, CausewayInteractionControllerElement],
[CAUSEWAY_FIELD_EDITOR, CausewayFieldEditorElement],
@@ -70,6 +72,7 @@ export function defineCausewayWebComponents(registry = globalThis.customElements
if (!registry) {
return;
}
+ captureDeclarativeActionParameters();
captureDeclarativeCollectionColumns();
for (const [name, constructor] of DEFINITIONS) {
if (!registry.get(name)) {
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index e3ee528fb33..5a51466eece 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -25,10 +25,14 @@ import {CAUSEWAY_COMPONENT_STYLES} from '../src/component-styles.mjs';
test('external structural stylesheet remains synchronized with the installable styles', async () => {
const external = await readFile(new URL('../src/component-styles.css', import.meta.url), 'utf8');
- const withoutLicense = external.slice(external.indexOf('cw-object {')).trim();
+ const withoutLicense = external.slice(external.indexOf('cw-parameter {')).trim();
assert.equal(withoutLicense, CAUSEWAY_COMPONENT_STYLES.trim());
});
+test('declarative action parameters remain non-visual configuration elements', () => {
+ assert.match(CAUSEWAY_COMPONENT_STYLES, /^\s*cw-parameter \{\s+display: none;\s+\}/);
+});
+
test('property edit affordance remains a compact deterministic icon control', async () => {
assert.match(CAUSEWAY_COMPONENT_STYLES, /\.causeway-property-edit \{[\s\S]*?block-size: 2rem;[\s\S]*?inline-size: 2rem;[\s\S]*?padding: 0\.35rem;/);
assert.match(CAUSEWAY_COMPONENT_STYLES, /\.causeway-property-edit-icon \{[\s\S]*?block-size: 1rem;[\s\S]*?inline-size: 1rem;[\s\S]*?stroke: currentColor;/);
diff --git a/viewers/webcomponents/foundation/test/components.test.mjs b/viewers/webcomponents/foundation/test/components.test.mjs
index be121e9ed26..0237be86264 100644
--- a/viewers/webcomponents/foundation/test/components.test.mjs
+++ b/viewers/webcomponents/foundation/test/components.test.mjs
@@ -29,6 +29,7 @@ const {
CausewayCollectionColumnElement,
CausewayCollectionElement,
CausewayObjectContextElement,
+ CausewayParameterElement,
CausewayPropertyElement,
CausewaySemanticEvent,
CausewayValueRendererRegistry,
@@ -38,6 +39,38 @@ const {
defineCausewayWebComponents();
+test('action parameter configuration normalizes optional presentation hints', () => {
+ const parameter = new CausewayParameterElement();
+ parameter.id = 'firstName';
+ parameter.named = 'Given name';
+ parameter.describedAs = 'The given name';
+ parameter.descriptionAs = 'ToOlTiP';
+ parameter.multiLine = 500;
+
+ assert.deepEqual(parameter.configuration, {
+ parameter: 'firstName',
+ named: 'Given name',
+ describedAs: 'The given name',
+ descriptionAs: 'tooltip',
+ multiLine: 50
+ });
+ assert.equal(parameter.hidden, false);
+ parameter.connectedCallback();
+ assert.equal(parameter.hidden, true);
+
+ parameter.removeAttribute('named');
+ parameter.removeAttribute('described-as');
+ parameter.removeAttribute('description-as');
+ parameter.setAttribute('multi-line', 'invalid');
+ assert.deepEqual(parameter.configuration, {
+ parameter: 'firstName',
+ named: null,
+ describedAs: null,
+ descriptionAs: null,
+ multiLine: null
+ });
+});
+
test('breadcrumbs render accessible ancestor links, escaped current state and local errors', () => {
const breadcrumbs = new CausewayBreadcrumbsElement();
breadcrumbs.renderComponentState(state({
@@ -348,6 +381,33 @@ test('property presentation selects every qualified field family and preserves d
test('action names descriptions disabled reasons and Font Awesome hints share one presentation', () => {
const action = new CausewayActionElement();
action.id = 'placeOrder';
+ const parameter = new CausewayParameterElement();
+ parameter.id = 'notes';
+ parameter.named = 'Order notes';
+ parameter.describedAs = 'Details for fulfilment';
+ parameter.descriptionAs = 'tooltip';
+ parameter.multiLine = 4;
+ action.appendChild(parameter);
+ document.body.appendChild(action);
+ assert.deepEqual(action.parameterPresentations, [{
+ parameter: 'notes',
+ named: 'Order notes',
+ describedAs: 'Details for fulfilment',
+ descriptionAs: 'tooltip',
+ multiLine: 4
+ }]);
+ const updatedParameter = new CausewayParameterElement();
+ updatedParameter.id = 'notes';
+ updatedParameter.named = 'Revised order notes';
+ action.appendChild(updatedParameter);
+ assert.deepEqual(action.parameterPresentations, [{
+ parameter: 'notes',
+ named: 'Revised order notes',
+ describedAs: null,
+ descriptionAs: null,
+ multiLine: null
+ }]);
+ document.body.removeChild(action);
action.label = 'Legacy order label';
action.named = 'Place a new order';
action.context = {identity: {logicalTypeName: 'example.Order', id: '42'}};
@@ -385,6 +445,7 @@ test('action names descriptions disabled reasons and Font Awesome hints share on
assert.equal(action.activate(), true);
assert.equal(request.presentation.name, 'Place a new order');
assert.equal(request.presentation.description, 'Creates an order for this customer.');
+ assert.deepEqual(request.presentation.parameters, action.parameterPresentations);
});
test('member-bearing elements use native identifiers without a member compatibility API', () => {
diff --git a/viewers/webcomponents/foundation/test/domain-components.test.mjs b/viewers/webcomponents/foundation/test/domain-components.test.mjs
index 8aee6386910..abd47f11ff5 100644
--- a/viewers/webcomponents/foundation/test/domain-components.test.mjs
+++ b/viewers/webcomponents/foundation/test/domain-components.test.mjs
@@ -59,6 +59,7 @@ test('registers the complete compact custom-element vocabulary without old alias
'cw-value',
'cw-object-link',
'cw-action',
+ 'cw-parameter',
'cw-interaction-controller',
'cw-reference-editor',
'cw-collection',
diff --git a/viewers/webcomponents/foundation/test/element-prefix.test.mjs b/viewers/webcomponents/foundation/test/element-prefix.test.mjs
index 451f480c25b..c5c4e1c3f33 100644
--- a/viewers/webcomponents/foundation/test/element-prefix.test.mjs
+++ b/viewers/webcomponents/foundation/test/element-prefix.test.mjs
@@ -53,6 +53,7 @@ const OLD_SUFFIXES = [
'value',
'object-link',
'action',
+ 'parameter',
'interaction-controller',
'field-editor',
'reference-editor',
@@ -69,7 +70,7 @@ const SUFFIX_PATTERN = [...OLD_SUFFIXES]
const OLD_TAG = new RegExp(`(?:<|<)\\/?${OLD_PREFIX}(?:${SUFFIX_PATTERN})(?![a-z0-9-])`);
const OLD_API_NAME = new RegExp(`(?<![.\\w-])${OLD_PREFIX}(?:${SUFFIX_PATTERN})(?![a-z0-9-])`);
const OLD_TYPE_SELECTOR = new RegExp(`(?:^|[>,(]\\s*)${OLD_PREFIX}(?:${SUFFIX_PATTERN})(?=[\\s\\[{:>,])`);
-const MEMBER_BEARING_SUFFIX_PATTERN = 'property|action|collection|collection-column';
+const MEMBER_BEARING_SUFFIX_PATTERN = 'property|action|parameter|collection|collection-column';
const OLD_MEMBER_TAG = new RegExp(`<cw-(?:${MEMBER_BEARING_SUFFIX_PATTERN})\\b[^>]*\\bmember\\s*=`);
const OLD_MEMBER_SELECTOR = new RegExp(`cw-(?:${MEMBER_BEARING_SUFFIX_PATTERN})[^\\n]*\\[member(?:[~|^$*]?=|\\])`);
const OLD_MEMBER_DOM_API = /(?:get|set)Attribute(?:\?\.)?\(\s*['"]member['"]/;
@@ -158,9 +159,10 @@ test('application markup does not expose raw Vaadin elements', async () => {
});
test('compact element names retain established non-element Causeway contracts', () => {
- assert.equal(Object.values(CausewayElementName).length, 17);
+ assert.equal(Object.values(CausewayElementName).length, 18);
assert.ok(Object.values(CausewayElementName).every(name => name.startsWith('cw-')));
assert.equal(CAUSEWAY_FIELD_EDITOR, 'cw-field-editor');
assert.equal(CausewayHostClass.PROPERTY, 'causeway-property');
+ assert.equal(CausewayHostClass.PARAMETER, 'causeway-parameter');
assert.equal(CausewaySemanticEvent.PROPERTY_UPDATED, 'causeway-property-updated');
});
diff --git a/viewers/webcomponents/foundation/test/interaction.test.mjs b/viewers/webcomponents/foundation/test/interaction.test.mjs
index cb57617bb47..e894643c307 100644
--- a/viewers/webcomponents/foundation/test/interaction.test.mjs
+++ b/viewers/webcomponents/foundation/test/interaction.test.mjs
@@ -768,6 +768,38 @@ test('standard action controller renders prompts, blocks invalid input, invokes
assert.ok(calls.includes('invoke:Updated'));
});
+test('action prompt applies partial authored parameter presentation without changing authority', async () => {
+ const parameters = [
+ {id: 'notes', description: 'Canonical notes', inputType: scalar('String'), enumValues: [], fields: new Map(), state: {hidden: false, disabled: null, validity: null}},
+ {id: 'reason', description: 'Canonical reason', inputType: scalar('String'), enumValues: [], fields: new Map(), state: {hidden: false, disabled: null, validity: null}},
+ {id: 'species', description: 'Species', inputType: scalar('String'), enumValues: [], fields: new Map(), state: {hidden: false, disabled: null, validity: null}}
+ ];
+ const context = {
+ async prepareAction() {
+ return {status: 'success', errors: [], data: {parameters}};
+ }
+ };
+ const controller = new CausewayInteractionControllerElement();
+ document.body.appendChild(controller);
+ assert.equal(await controller.beginAction('customize', context, null, {
+ parameters: [
+ {parameter: 'notes', named: 'Visit notes', describedAs: 'Private <clinical> context', descriptionAs: 'tooltip', multiLine: 4},
+ {parameter: 'reason', named: null, describedAs: 'Explain the appointment', descriptionAs: 'label', multiLine: null},
+ {parameter: 'unknown', named: 'Must not render', describedAs: null, descriptionAs: null, multiLine: null}
+ ]
+ }), true);
+
+ assert.match(controller.innerHTML, /data-parameter="notes" data-multi-line="4"/);
+ assert.match(controller.innerHTML, /class="causeway-action-parameter-label causeway-member-tooltip"[^>]+data-tooltip="Private <clinical> context"[^>]*>Visit notes<\/label>/);
+ assert.match(controller.innerHTML, /causeway-action-parameter-description causeway-visually-hidden">Private <clinical> context<\/span>/);
+ assert.match(controller.innerHTML, /data-control="text-area"[^>]+data-rows="4"/);
+ assert.match(controller.innerHTML, /data-parameter="reason"[\s\S]*?>Reason<\/label>[\s\S]*?causeway-action-parameter-description">Explain the appointment<\/span>/);
+ assert.match(controller.innerHTML, /data-parameter="species"[\s\S]*?>Species<\/label>/);
+ assert.doesNotMatch(controller.innerHTML, /Must not render|data-parameter="unknown"/);
+ assert.deepEqual(controller.promptState.parameters, parameters);
+ controller.cancelPrompt();
+});
+
test('action prompt defers prepared validity and recomputation until focus completion', async () => {
const calls = [];
const target = (id, value) => ({
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 3b6fc130ab7..db64fb102de 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
@@ -28,7 +28,12 @@
<h2 id="petclinic-owner-identity-heading">Identity</h2>
<div class="petclinic-field-list">
<cw-property id="name" named="Full name">
- <cw-action id="updateName" named="Change the owner's name"></cw-action>
+ <cw-action id="updateName" named="Change the owner's name">
+ <cw-parameter id="name"
+ named="Owner's full name"
+ described-as="The complete name used to identify this pet owner."
+ description-as="tooltip"></cw-parameter>
+ </cw-action>
</cw-property>
<cw-property id="knownAs" editable
described-as="The familiar or preferred name used by this owner."></cw-property>
@@ -60,7 +65,12 @@ <h2 id="petclinic-owner-pets-heading">Pets</h2>
<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>
- <cw-action id="addPet" named="Register a pet"></cw-action>
+ <cw-action id="addPet" named="Register a pet">
+ <cw-parameter id="name"
+ named="Pet name"
+ described-as="The name used for this companion animal."
+ description-as="label"></cw-parameter>
+ </cw-action>
<cw-action id="removePet"></cw-action>
</cw-collection>
</section>
@@ -73,7 +83,13 @@ <h2 id="petclinic-owner-visits-heading">Visits</h2>
<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>
- <cw-action id="bookVisit"></cw-action>
+ <cw-action id="bookVisit">
+ <cw-parameter id="reason"
+ named="Reason for visit"
+ described-as="Describe the purpose of the appointment."
+ description-as="label"
+ multi-line="3"></cw-parameter>
+ </cw-action>
</cw-collection>
</section>
</div>
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 8a36fab0c18..e79716c7699 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
@@ -592,6 +592,12 @@ void propertyEditingAndPromptFocusRemainDeterministic() {
waitForPrompt("updateName", "Change the owner's name");
assertThat(page.locator(PROMPT + " .causeway-action-prompt-description").textContent())
.isEqualTo("Updates the owner's full name.");
+ final var updateNameLabel = page.locator(PROMPT + " [data-parameter='name'] .causeway-action-parameter-label");
+ assertThat(updateNameLabel.textContent()).isEqualTo("Owner's full name");
+ assertThat(updateNameLabel.getAttribute("data-tooltip"))
+ .isEqualTo("The complete name used to identify this pet owner.");
+ assertThat(page.locator(PROMPT + " [data-parameter='name'] .causeway-action-parameter-description")
+ .getAttribute("class")).contains("causeway-visually-hidden");
assertFocused(parameter("name"));
fillParameter("name", "Invalid % name");
submitPrompt();
@@ -642,6 +648,12 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
waitForPrompt("addPet", "Register a pet");
assertThat(page.locator(PROMPT + " .causeway-action-prompt-description").textContent())
.isEqualTo("Adds a pet to this owner's household.");
+ assertThat(page.locator(PROMPT + " [data-parameter='name'] .causeway-action-parameter-label").textContent())
+ .isEqualTo("Pet name");
+ assertThat(page.locator(PROMPT + " [data-parameter='name'] .causeway-action-parameter-description").textContent())
+ .isEqualTo("The name used for this companion animal.");
+ assertThat(page.locator(PROMPT + " [data-parameter='species'] .causeway-action-parameter-label").textContent())
+ .isEqualTo("Species");
assertThat(page.locator(PROMPT + " .causeway-action-parameter-reason").count()).isZero();
final var preparationsBeforeName = graphQLOperationCount("CausewayPrepareAction");
tabOutOfParameter("name");
@@ -695,6 +707,12 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
}
final var visitAt = resolveEditor(parameter("visitAt"));
final var reason = resolveEditor(parameter("reason"));
+ assertThat(page.locator(PROMPT + " [data-parameter='reason'] .causeway-action-parameter-label").textContent())
+ .isEqualTo("Reason for visit");
+ assertThat(page.locator(PROMPT + " [data-parameter='reason'] .causeway-action-parameter-description").textContent())
+ .isEqualTo("Describe the purpose of the appointment.");
+ assertThat(reason.evaluate("element => element.localName === 'vaadin-text-area' ? element.maxRows : Number(element.rows)"))
+ .isEqualTo(3);
assertThat(visitAt.count())
.as(page.locator(PROMPT).evaluate("element => element.outerHTML").toString())
.isEqualTo(1);
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 f61b01fd86f..d8afb38dfbc 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
@@ -113,9 +113,11 @@ void servesStableShellCanonicalFragmentsAssetsAndWicketComparison() throws Excep
assertThat(get("/htmx/object/petclinic.PetOwner/s_owner-mary").body())
.contains("<cw-action id=\"delete\" named=\"Remove this owner\"")
.contains("<cw-action id=\"updateName\" named=\"Change the owner's name\"")
+ .contains("<cw-parameter id=\"name\"")
+ .contains("named=\"Owner's full name\"")
.contains("<cw-action id=\"addPet\" named=\"Register a pet\"")
.contains("<cw-action id=\"removePet\"></cw-action>")
- .contains("<cw-action id=\"bookVisit\"></cw-action>");
+ .contains("<cw-action id=\"bookVisit\">");
assertResourcePage(
"/htmx/object/petclinic.Pet/s_pet-basil",
"petclinic.Pet",
@@ -228,6 +230,12 @@ void packagesHtmlPagesAndRetainsLayoutFallbackResources() throws Exception {
assertThat(ownerHtml)
.contains("<div class=\"petclinic-object-heading\">\n <cw-object-header></cw-object-header>\n <div class=\"petclinic-page-toolbar\" aria-label=\"Owner actions\">\n <cw-action id=\"delete\"")
.contains("<cw-property id=\"name\" named=\"Full name\">\n <cw-action id=\"updateName\"")
+ .contains("<cw-parameter id=\"name\"\n named=\"Owner's full name\"")
+ .contains("description-as=\"tooltip\"")
+ .contains("<cw-parameter id=\"name\"\n named=\"Pet name\"")
+ .contains("<cw-parameter id=\"reason\"\n named=\"Reason for visit\"")
+ .contains("multi-line=\"3\"")
+ .doesNotContain("<cw-parameter id=\"species\"", "<cw-action id=\"removePet\">\n <cw-parameter")
.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\"")
(causeway) 02/05: docs(openspec): plan action parameter component
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 5580941253055e2e3c885f3946c75c33a07477bc
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 19:22:20 2026 +0200
docs(openspec): plan action parameter component
---
.../add-action-parameter-component/.openspec.yaml | 2 +
.../add-action-parameter-component/design.md | 86 ++++++++++++++++++++++
.../add-action-parameter-component/proposal.md | 32 ++++++++
.../specs/domain-web-components/spec.md | 56 ++++++++++++++
.../generic-htmx-web-component-viewer/spec.md | 19 +++++
.../add-action-parameter-component/tasks.md | 23 ++++++
6 files changed, 218 insertions(+)
diff --git a/openspec/changes/add-action-parameter-component/.openspec.yaml b/openspec/changes/add-action-parameter-component/.openspec.yaml
new file mode 100644
index 00000000000..88ea4823207
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-30
diff --git a/openspec/changes/add-action-parameter-component/design.md b/openspec/changes/add-action-parameter-component/design.md
new file mode 100644
index 00000000000..80a80f639bf
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/design.md
@@ -0,0 +1,86 @@
+## Context
+
+The standard interaction controller currently derives every action-parameter label by humanizing the GraphQL parameter id, renders the canonical description visibly, and selects an editor solely from the introspected parameter shape.
+Custom pages can author presentation attributes on `<cw-property>`, `<cw-action>`, and `<cw-collection>`, but an authored `<cw-action>` has no declarative way to refine selected parameter presentation.
+`<cw-action>` replaces its light-DOM content when rendering, so nested configuration elements must be captured before ordinary action markup replaces them.
+The existing collection-column capture and configuration pattern provides a precedent for declarative, non-visual child components.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Register and export a public `<cw-parameter>` configuration component.
+- Let an authored action declare zero, some, or all parameter presentation overrides by semantic parameter id.
+- Support property-consistent `named`, `described-as`, `description-as="label|tooltip"`, and bounded `multi-line` behavior.
+- Preserve canonical parameter semantics for omitted declarations and omitted attributes.
+- Keep the standard controller authoritative for prompt lifecycle and the object context authoritative for GraphQL operations.
+- Demonstrate partial declarations on selected Petclinic actions.
+
+**Non-Goals:**
+
+- Declare, add, remove, reorder, hide, disable, default, constrain, validate, or invoke parameters from HTML.
+- Change GraphQL parameter metadata or operation planning.
+- Require `<cw-parameter>` children for parameterized actions.
+- Add label-position overrides for action parameters.
+- Expose application-authored raw toolkit controls.
+
+## Decisions
+
+### Model `<cw-parameter>` as a non-visual configuration component
+
+`CausewayParameterElement` will extend the environment-safe HTMLElement base used by other configuration elements and expose normalized immutable configuration.
+It will observe `id`, `named`, `described-as`, `description-as`, and `multi-line`, remain hidden, and notify its owning action when connected or changed.
+The public element name, host contract, attribute constants, registration, module export, and documentation will be updated.
+
+A visible standalone parameter component was rejected because action parameters require an active prompt, pending argument set, and controller-owned validation lifecycle.
+
+### Capture declarative children before action rendering
+
+Registration will capture pre-upgrade `<cw-parameter>` children, and `CausewayActionElement` will capture already-connected or dynamically added parameter configurations before replacing ordinary action markup.
+The action will retain immutable configurations keyed by non-empty semantic parameter id and include them in the existing action presentation carried by the semantic action-request event.
+Unknown ids remain harmless presentation data and are ignored when no authoritative parameter matches.
+Duplicate ids resolve deterministically to the latest accepted configuration so dynamic attribute updates replace rather than multiply hints.
+
+Preserving the configuration elements as visible prompt children was rejected because it would give two elements ownership of editor and validation state.
+Constructing parameter GraphQL requirements from declarations was rejected because declarations are optional presentation hints, not semantic authority.
+
+### Merge hints only at prompt presentation boundaries
+
+The interaction controller will retain normalized parameter presentation hints separately from GraphQL-authoritative parameter state.
+For each prepared parameter it will resolve:
+
+- label from explicit `named`, otherwise the established humanized id;
+- description from explicit `described-as`, otherwise the authoritative parameter description;
+- description presentation from explicit `description-as`, otherwise visible label presentation;
+- multiline rows from a valid explicit `multi-line`, otherwise ordinary editor selection.
+
+The effective label and description will feed editor accessibility ids and names.
+Tooltip descriptions will use the established bounded member-tooltip presentation while retaining visually hidden accessible description text.
+The effective multiline value will be passed into the standard editor registry, which already selects textarea presentation for supported strings.
+Hidden, disabled, default, choices, autocomplete, validity, parameter order, and invocation values continue to come exclusively from each prepared GraphQL parameter.
+
+Mutating the prepared parameter objects was rejected because recomputation replaces those objects and because presentation should remain distinguishable from authoritative interaction state.
+
+### Demonstrate selective Petclinic overrides
+
+Petclinic will add representative nested declarations to selected existing parameterized actions while deliberately leaving sibling parameters and other actions undeclared.
+The sample will cover an authored name, authored description in visible and tooltip forms, and multiline string editing without changing domain methods.
+Resource-page, foundation interaction, and browser or integration assertions will verify declarative structure and effective prompt presentation.
+
+## Risks / Trade-offs
+
+- [Risk] Action rendering can erase declarative children before configuration is read. → Capture before custom-element registration and again during action connection, following the collection-column precedent.
+- [Risk] Presentation hints could accidentally become semantic parameter declarations. → Match only ids returned by authoritative preparation and never alter parameter arrays or operation values.
+- [Risk] Recomputed parameter state could lose presentation. → Store normalized hints in prompt state independently and resolve them for every render.
+- [Risk] Tooltip descriptions could disappear from accessibility APIs. → Keep a visually hidden description node and wire editor `aria-describedby` while exposing the label tooltip by pointer and keyboard.
+- [Risk] Multiline could be applied to an incompatible type. → Pass the bounded hint through existing editor qualification so unsupported shapes retain their normal editor.
+
+## Migration Plan
+
+No migration is required.
+Existing actions without `<cw-parameter>` children retain byte-equivalent semantic behavior and established prompt presentation.
+Removing nested declarations restores canonical presentation without changing domain code or GraphQL metadata.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/add-action-parameter-component/proposal.md b/openspec/changes/add-action-parameter-component/proposal.md
new file mode 100644
index 00000000000..b28e7210484
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/proposal.md
@@ -0,0 +1,32 @@
+## Why
+
+Custom HTML pages can override action-level presentation but cannot currently refine individual action-parameter labels, descriptions, description presentation, or multiline editing.
+A declarative `<cw-parameter>` child gives applications selective presentation control while retaining GraphQL-authoritative parameter semantics and allowing unspecified parameters to use canonical metadata.
+
+## What Changes
+
+- Add a public framework-neutral `<cw-parameter id="parameterName">` custom element that may be nested directly beneath `<cw-action>`.
+- Treat nested parameter elements as optional presentation hints matched by semantic parameter id; actions do not require every parameter to be declared.
+- Support `named`, `described-as`, `description-as="label|tooltip"`, and bounded `multi-line` attributes with behavior consistent with property presentation overrides.
+- Apply matching hints when the standard interaction controller renders and edits action parameters without changing hidden, disabled, default, choice, autocomplete, validation, ordering, or invocation semantics.
+- Preserve canonical metadata and editor behavior for undeclared parameters and for attributes not explicitly overridden.
+- Extend selected Petclinic parameterized actions with representative, partial `<cw-parameter>` declarations and regression coverage.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `domain-web-components`: Add the semantic action-parameter component vocabulary and selective parameter presentation override contract.
+- `generic-htmx-web-component-viewer`: Extend Petclinic custom-page and browser qualification with representative partial action-parameter declarations.
+
+## Impact
+
+- Foundation custom-element contracts, registration, exports, action rendering, action-request presentation payload, and interaction prompt rendering.
+- Shared presentation normalization for names, descriptions, tooltip presentation, and multiline editor hints.
+- Foundation structural styles, documentation, and component/interaction tests.
+- Selected Petclinic HTML pages and integration or browser regression coverage.
+- No GraphQL schema, operation placement, parameter identity, parameter ordering, validation, or invocation API changes.
diff --git a/openspec/changes/add-action-parameter-component/specs/domain-web-components/spec.md b/openspec/changes/add-action-parameter-component/specs/domain-web-components/spec.md
new file mode 100644
index 00000000000..8c8949ae80a
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/specs/domain-web-components/spec.md
@@ -0,0 +1,56 @@
+## ADDED Requirements
+
+### Requirement: Declarative action-parameter presentation
+The component library SHALL provide a framework-neutral `<cw-parameter>` configuration component that MAY be nested directly beneath `<cw-action>` to supply presentation hints for the authoritative action parameter with the same semantic id.
+Nested parameter declarations MUST NOT add, remove, reorder, hide, disable, default, validate, or invoke parameters.
+
+#### Scenario: Selected parameter has authored presentation
+- **WHEN** an authored action contains `<cw-parameter id="firstName" named="Given name" described-as="The person's given name" description-as="label" multi-line="3">`
+- **AND** authoritative action preparation returns a compatible parameter with id `firstName`
+- **THEN** the standard prompt uses the authored name and description and a bounded multiline editor
+- **AND** hidden, disabled, default, choice, autocomplete, validity, order, and invocation semantics remain authoritative
+
+#### Scenario: Parameter declaration is partial
+- **WHEN** an action declares presentation for only some authoritative parameters
+- **THEN** matching declared parameters use only their explicitly supplied overrides
+- **AND** undeclared parameters and non-overridden presentation fields retain established canonical behavior
+
+#### Scenario: Declared parameter is not authoritative
+- **WHEN** a nested parameter id is absent from authoritative action preparation
+- **THEN** the controller ignores that declaration
+- **AND** it does not create an editor, argument, validation request, or invocation value for that id
+
+#### Scenario: Action declares no parameters
+- **WHEN** an existing authored or generated action has no nested `<cw-parameter>` children
+- **THEN** parameter preparation, prompt presentation, validation, and invocation remain unchanged
+
+### Requirement: Action-parameter name and description overrides
+`<cw-parameter>` SHALL support optional `named`, `described-as`, and `description-as` attributes with normalization and accessibility behavior consistent with property presentation.
+`description-as` MUST accept `label` and `tooltip`, MUST default to `label`, and MUST fall back safely for blank or unsupported values.
+
+#### Scenario: Visible authored description
+- **WHEN** a matching declaration supplies `described-as` and `description-as="label"`
+- **THEN** the effective description appears in the parameter layout
+- **AND** the editor is associated with that description accessibly
+
+#### Scenario: Tooltip authored description
+- **WHEN** a matching declaration supplies `described-as` and `description-as="tooltip"`
+- **THEN** the effective description is available from the parameter label by pointer and keyboard
+- **AND** hidden accessible description text remains associated with the editor
+
+#### Scenario: Authored description duplicates effective name
+- **WHEN** the effective description equals the effective parameter name ignoring case and surrounding whitespace
+- **THEN** the prompt suppresses redundant visible and tooltip description presentation
+
+### Requirement: Action-parameter multiline override
+`<cw-parameter>` SHALL support an optional bounded `multi-line` attribute that requests the established multiline string editor without changing parameter value parsing or validation.
+
+#### Scenario: Valid multiline hint
+- **WHEN** a matching string parameter declaration supplies an integer `multi-line` value greater than one within or above the supported bound
+- **THEN** the standard editor uses a textarea with the bounded effective row count
+- **AND** pending-value parsing, focus, validation, and invocation remain identical to an ordinary string parameter
+
+#### Scenario: Invalid or incompatible multiline hint
+- **WHEN** `multi-line` is absent, malformed, non-positive, one, or incompatible with the authoritative input shape
+- **THEN** the parameter retains its established qualified editor
+- **AND** unrelated parameter presentation remains available
diff --git a/openspec/changes/add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
new file mode 100644
index 00000000000..6fd89b3741c
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
@@ -0,0 +1,19 @@
+## ADDED Requirements
+
+### Requirement: Petclinic selective action-parameter presentation
+Petclinic custom HTML pages SHALL demonstrate optional `<cw-parameter>` presentation declarations on selected parameterized actions while leaving other parameters and actions canonical.
+
+#### Scenario: Maintainer inspects Petclinic HTML
+- **WHEN** a maintainer opens the packaged Petclinic custom-page resources
+- **THEN** selected `<cw-action>` elements contain natural nested `<cw-parameter>` declarations using representative `named`, `described-as`, `description-as`, and `multi-line` attributes
+- **AND** at least one sibling parameter and at least one other parameterized action remain undeclared
+
+#### Scenario: Selected Petclinic action prompt opens
+- **WHEN** browser automation activates an action with matching parameter declarations
+- **THEN** the prompt applies the authored parameter name, description presentation, and compatible multiline hint
+- **AND** canonical defaults, choices, validation, invocation, result, focus, and route behavior remain unchanged
+
+#### Scenario: Undeclared Petclinic parameter renders
+- **WHEN** the same or another Petclinic prompt contains an authoritative parameter without a matching declaration
+- **THEN** that parameter retains its established canonical name, description, and qualified editor
+- **AND** no HTML declaration is required for it to participate in validation or invocation
diff --git a/openspec/changes/add-action-parameter-component/tasks.md b/openspec/changes/add-action-parameter-component/tasks.md
new file mode 100644
index 00000000000..aefd86dac4e
--- /dev/null
+++ b/openspec/changes/add-action-parameter-component/tasks.md
@@ -0,0 +1,23 @@
+## 1. Public parameter component
+
+- [ ] 1.1 Add the `<cw-parameter>` element with normalized immutable `named`, `described-as`, `description-as`, and bounded `multi-line` configuration.
+- [ ] 1.2 Register and export the element and extend public element, host, attribute, and configuration-event contracts.
+- [ ] 1.3 Add synchronized structural styling and public usage documentation for the non-visual nested configuration component.
+
+## 2. Action and prompt integration
+
+- [ ] 2.1 Capture initial and dynamic parameter declarations in `<cw-action>` without making them authoritative action parameters.
+- [ ] 2.2 Carry immutable parameter presentation hints through semantic action requests into standard prompt state.
+- [ ] 2.3 Merge matching names, descriptions, label-or-tooltip presentation, and multiline rows into parameter markup and editor context while preserving canonical interaction state.
+- [ ] 2.4 Add component and interaction tests for partial declarations, unknown ids, fallback behavior, accessibility, dynamic updates, and multiline qualification.
+
+## 3. Petclinic demonstration
+
+- [ ] 3.1 Add representative partial `<cw-parameter>` declarations to selected Petclinic parameterized actions while leaving other parameters and actions undeclared.
+- [ ] 3.2 Extend Petclinic resource, integration, and applicable browser assertions for authored and canonical parameter presentation.
+
+## 4. Verification
+
+- [ ] 4.1 Run the complete foundation test suite.
+- [ ] 4.2 Run the applicable Petclinic Maven tests.
+- [ ] 4.3 Run strict OpenSpec validation and verify synchronized installable styles.
(causeway) 04/05: docs(openspec): sync action parameter specifications
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 6a76d6bb787d48edeed012caa9f77fd4caa282c6
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 19:36:51 2026 +0200
docs(openspec): sync action parameter specifications
---
openspec/specs/domain-web-components/spec.md | 55 ++++++++++++++++++++++
.../generic-htmx-web-component-viewer/spec.md | 18 +++++++
2 files changed, 73 insertions(+)
diff --git a/openspec/specs/domain-web-components/spec.md b/openspec/specs/domain-web-components/spec.md
index d21df7ce6e3..961cc86886e 100644
--- a/openspec/specs/domain-web-components/spec.md
+++ b/openspec/specs/domain-web-components/spec.md
@@ -1915,3 +1915,58 @@ The component library SHALL place actions authored directly beneath a property i
#### Scenario: Associated action is activated
- **WHEN** a user activates a field-aligned associated action
- **THEN** the established semantic action request, toolkit selection, focus, disabled state, tooltip, and invocation behavior remain unchanged
+
+### Requirement: Declarative action-parameter presentation
+The component library SHALL provide a framework-neutral `<cw-parameter>` configuration component that MAY be nested directly beneath `<cw-action>` to supply presentation hints for the authoritative action parameter with the same semantic id.
+Nested parameter declarations MUST NOT add, remove, reorder, hide, disable, default, validate, or invoke parameters.
+
+#### Scenario: Selected parameter has authored presentation
+- **WHEN** an authored action contains `<cw-parameter id="firstName" named="Given name" described-as="The person's given name" description-as="label" multi-line="3">`
+- **AND** authoritative action preparation returns a compatible parameter with id `firstName`
+- **THEN** the standard prompt uses the authored name and description and a bounded multiline editor
+- **AND** hidden, disabled, default, choice, autocomplete, validity, order, and invocation semantics remain authoritative
+
+#### Scenario: Parameter declaration is partial
+- **WHEN** an action declares presentation for only some authoritative parameters
+- **THEN** matching declared parameters use only their explicitly supplied overrides
+- **AND** undeclared parameters and non-overridden presentation fields retain established canonical behavior
+
+#### Scenario: Declared parameter is not authoritative
+- **WHEN** a nested parameter id is absent from authoritative action preparation
+- **THEN** the controller ignores that declaration
+- **AND** it does not create an editor, argument, validation request, or invocation value for that id
+
+#### Scenario: Action declares no parameters
+- **WHEN** an existing authored or generated action has no nested `<cw-parameter>` children
+- **THEN** parameter preparation, prompt presentation, validation, and invocation remain unchanged
+
+### Requirement: Action-parameter name and description overrides
+`<cw-parameter>` SHALL support optional `named`, `described-as`, and `description-as` attributes with normalization and accessibility behavior consistent with property presentation.
+`description-as` MUST accept `label` and `tooltip`, MUST default to `label`, and MUST fall back safely for blank or unsupported values.
+
+#### Scenario: Visible authored description
+- **WHEN** a matching declaration supplies `described-as` and `description-as="label"`
+- **THEN** the effective description appears in the parameter layout
+- **AND** the editor is associated with that description accessibly
+
+#### Scenario: Tooltip authored description
+- **WHEN** a matching declaration supplies `described-as` and `description-as="tooltip"`
+- **THEN** the effective description is available from the parameter label by pointer and keyboard
+- **AND** hidden accessible description text remains associated with the editor
+
+#### Scenario: Authored description duplicates effective name
+- **WHEN** the effective description equals the effective parameter name ignoring case and surrounding whitespace
+- **THEN** the prompt suppresses redundant visible and tooltip description presentation
+
+### Requirement: Action-parameter multiline override
+`<cw-parameter>` SHALL support an optional bounded `multi-line` attribute that requests the established multiline string editor without changing parameter value parsing or validation.
+
+#### Scenario: Valid multiline hint
+- **WHEN** a matching string parameter declaration supplies an integer `multi-line` value greater than one within or above the supported bound
+- **THEN** the standard editor uses a textarea with the bounded effective row count
+- **AND** pending-value parsing, focus, validation, and invocation remain identical to an ordinary string parameter
+
+#### Scenario: Invalid or incompatible multiline hint
+- **WHEN** `multi-line` is absent, malformed, non-positive, one, or incompatible with the authoritative input shape
+- **THEN** the parameter retains its established qualified editor
+- **AND** unrelated parameter presentation remains available
diff --git a/openspec/specs/generic-htmx-web-component-viewer/spec.md b/openspec/specs/generic-htmx-web-component-viewer/spec.md
index e07422f43c9..8e93f7dc41c 100644
--- a/openspec/specs/generic-htmx-web-component-viewer/spec.md
+++ b/openspec/specs/generic-htmx-web-component-viewer/spec.md
@@ -790,3 +790,21 @@ The Petclinic owner custom page SHALL compose its object title and object-level
- **WHEN** the owner page composes the title and object-level action
- **THEN** placement is defined by the Petclinic HTML resource and application stylesheet
- **AND** `<cw-object-header>` and `<cw-action>` retain their established framework-neutral rendering and semantic behavior
+
+### Requirement: Petclinic selective action-parameter presentation
+Petclinic custom HTML pages SHALL demonstrate optional `<cw-parameter>` presentation declarations on selected parameterized actions while leaving other parameters and actions canonical.
+
+#### Scenario: Maintainer inspects Petclinic HTML
+- **WHEN** a maintainer opens the packaged Petclinic custom-page resources
+- **THEN** selected `<cw-action>` elements contain natural nested `<cw-parameter>` declarations using representative `named`, `described-as`, `description-as`, and `multi-line` attributes
+- **AND** at least one sibling parameter and at least one other parameterized action remain undeclared
+
+#### Scenario: Selected Petclinic action prompt opens
+- **WHEN** browser automation activates an action with matching parameter declarations
+- **THEN** the prompt applies the authored parameter name, description presentation, and compatible multiline hint
+- **AND** canonical defaults, choices, validation, invocation, result, focus, and route behavior remain unchanged
+
+#### Scenario: Undeclared Petclinic parameter renders
+- **WHEN** the same or another Petclinic prompt contains an authoritative parameter without a matching declaration
+- **THEN** that parameter retains its established canonical name, description, and qualified editor
+- **AND** no HTML declaration is required for it to participate in validation or invocation
(causeway) 01/05: fix(webcomponents): refine action layout spacing
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 1ee80212210d101eba6ebf2b7be976bf7333758c
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 18:59:01 2026 +0200
fix(webcomponents): refine action layout spacing
---
viewers/webcomponents/foundation/src/component-styles.css | 3 +++
viewers/webcomponents/foundation/src/component-styles.mjs | 3 +++
viewers/webcomponents/foundation/test/component-styles.test.mjs | 1 +
.../src/main/resources/static/css/application.css | 2 +-
.../sample/htmx/petclinic/PetClinicHtmxApplication_IntegTest.java | 1 +
5 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/viewers/webcomponents/foundation/src/component-styles.css b/viewers/webcomponents/foundation/src/component-styles.css
index 8367c1e3703..37cf9064537 100644
--- a/viewers/webcomponents/foundation/src/component-styles.css
+++ b/viewers/webcomponents/foundation/src/component-styles.css
@@ -81,6 +81,9 @@ cw-property[data-causeway-action-group] > cw-action[data-causeway-associated-act
margin-block-end: var(--causeway-associated-action-margin-block-end, 0.75rem);
min-inline-size: 0;
}
+cw-property[data-causeway-action-group] > cw-action[data-causeway-associated-action] > .causeway-action {
+ margin-block-start: var(--causeway-associated-action-margin-block-start, 0);
+}
cw-collection[data-causeway-action-group] > cw-action[data-causeway-associated-action] {
flex: 0 1 auto;
margin-block-end: var(--causeway-associated-action-margin-block-end, 0.75rem);
diff --git a/viewers/webcomponents/foundation/src/component-styles.mjs b/viewers/webcomponents/foundation/src/component-styles.mjs
index 821db6a7bad..e3cbe70904e 100644
--- a/viewers/webcomponents/foundation/src/component-styles.mjs
+++ b/viewers/webcomponents/foundation/src/component-styles.mjs
@@ -82,6 +82,9 @@ cw-property[data-causeway-action-group] > cw-action[data-causeway-associated-act
margin-block-end: var(--causeway-associated-action-margin-block-end, 0.75rem);
min-inline-size: 0;
}
+cw-property[data-causeway-action-group] > cw-action[data-causeway-associated-action] > .causeway-action {
+ margin-block-start: var(--causeway-associated-action-margin-block-start, 0);
+}
cw-collection[data-causeway-action-group] > cw-action[data-causeway-associated-action] {
flex: 0 1 auto;
margin-block-end: var(--causeway-associated-action-margin-block-end, 0.75rem);
diff --git a/viewers/webcomponents/foundation/test/component-styles.test.mjs b/viewers/webcomponents/foundation/test/component-styles.test.mjs
index 98efd7c1ac4..e3ee528fb33 100644
--- a/viewers/webcomponents/foundation/test/component-styles.test.mjs
+++ b/viewers/webcomponents/foundation/test/component-styles.test.mjs
@@ -106,6 +106,7 @@ test('direct member associations wrap in semantic source and keyboard order', ()
test('property-associated actions align with the effective field column responsively', async () => {
assert.match(CAUSEWAY_COMPONENT_STYLES, /cw-property\[data-causeway-action-group\] \{[\s\S]*?display: grid;[\s\S]*?grid-template-columns: var\(--causeway-property-label-column/);
assert.match(CAUSEWAY_COMPONENT_STYLES, /cw-property\[data-causeway-action-group\] > cw-action\[data-causeway-associated-action\] \{\s+grid-column: 2;/);
+ assert.match(CAUSEWAY_COMPONENT_STYLES, /> cw-action\[data-causeway-associated-action\] > \.causeway-action \{\s+margin-block-start: var\(--causeway-associated-action-margin-block-start, 0\);/);
const theme = await readFile(new URL('../src/theme.css', import.meta.url), 'utf8');
assert.match(theme, /cw-property\[data-causeway-action-group\] \{\s+grid-template-columns: minmax\(7rem, var\(--causeway-label-width\)\) minmax\(0, 1fr\) auto;/);
assert.match(theme, /cw-property\[data-causeway-action-group\] > cw-action\[data-causeway-associated-action\] \{\s+grid-column: 2 \/ -1;/);
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/static/css/application.css b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/static/css/application.css
index 4807f9a27d6..d6cfa3606a1 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/static/css/application.css
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/main/resources/static/css/application.css
@@ -117,7 +117,7 @@
border-block-end: 1px solid var(--causeway-border);
display: flex;
flex-wrap: wrap;
- gap: var(--causeway-space-4);
+ gap: var(--causeway-space-5);
min-width: 0;
}
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 3e5185d8ec6..f61b01fd86f 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
@@ -148,6 +148,7 @@ void servesStableShellCanonicalFragmentsAssetsAndWicketComparison() throws Excep
assertThat(get("/causeway-webcomponents/theme.css").statusCode()).isEqualTo(200);
assertThat(get("/css/application.css").body())
.contains(".petclinic-object-heading {")
+ .contains("gap: var(--causeway-space-5);")
.contains(".petclinic-object-heading > cw-object-header {")
.contains(".petclinic-object-heading .causeway-object-header {");
assertThat(get("/webjars/htmx.org/2.0.6/dist/htmx.min.js").statusCode()).isEqualTo(200);
(causeway) 05/05: docs(openspec): archive action parameter component
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 e0ea7ae517371384543e21adc0ab56b485150eec
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 30 19:37:37 2026 +0200
docs(openspec): archive action parameter component
---
.../2026-08-30-add-action-parameter-component}/.openspec.yaml | 0
.../2026-08-30-add-action-parameter-component}/design.md | 0
.../2026-08-30-add-action-parameter-component}/proposal.md | 0
.../specs/domain-web-components/spec.md | 0
.../specs/generic-htmx-web-component-viewer/spec.md | 0
.../2026-08-30-add-action-parameter-component}/tasks.md | 0
6 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/openspec/changes/add-action-parameter-component/.openspec.yaml b/openspec/changes/archive/2026-08-30-add-action-parameter-component/.openspec.yaml
similarity index 100%
rename from openspec/changes/add-action-parameter-component/.openspec.yaml
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/.openspec.yaml
diff --git a/openspec/changes/add-action-parameter-component/design.md b/openspec/changes/archive/2026-08-30-add-action-parameter-component/design.md
similarity index 100%
rename from openspec/changes/add-action-parameter-component/design.md
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/design.md
diff --git a/openspec/changes/add-action-parameter-component/proposal.md b/openspec/changes/archive/2026-08-30-add-action-parameter-component/proposal.md
similarity index 100%
rename from openspec/changes/add-action-parameter-component/proposal.md
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/proposal.md
diff --git a/openspec/changes/add-action-parameter-component/specs/domain-web-components/spec.md b/openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/domain-web-components/spec.md
similarity index 100%
rename from openspec/changes/add-action-parameter-component/specs/domain-web-components/spec.md
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/domain-web-components/spec.md
diff --git a/openspec/changes/add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md b/openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
similarity index 100%
rename from openspec/changes/add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/specs/generic-htmx-web-component-viewer/spec.md
diff --git a/openspec/changes/add-action-parameter-component/tasks.md b/openspec/changes/archive/2026-08-30-add-action-parameter-component/tasks.md
similarity index 100%
rename from openspec/changes/add-action-parameter-component/tasks.md
rename to openspec/changes/archive/2026-08-30-add-action-parameter-component/tasks.md