Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 65 additions & 38 deletions .claude/skills/create-core-component/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,7 @@ Every component MUST satisfy all applicable items below. This checklist is deriv

## BEM Naming Convention for Accessibility Elements

Every component follows this naming pattern:
```
.cmp-adaptiveform-{componentname} — root container
.cmp-adaptiveform-{componentname}__label — label element
.cmp-adaptiveform-{componentname}__widget — main interactive widget
.cmp-adaptiveform-{componentname}__errormessage — error message area
.cmp-adaptiveform-{componentname}__shortdescription — tooltip
.cmp-adaptiveform-{componentname}__longdescription — expanded help text
.cmp-adaptiveform-{componentname}__questionmark — help toggle button
.cmp-adaptiveform-{componentname}__label-container — label + question mark wrapper
```
For the standard BEM element set (`__label`, `__widget`, `__errormessage`, `__shortdescription`, `__longdescription`, `__questionmark`, `__label-container`), see the table in `references/css-architecture.md`. Every one of those elements must be present and correctly linked for accessibility.

## ID Convention for ARIA Linkage

Expand All @@ -97,9 +87,9 @@ common source of runtime bugs.

| Method | If you override it… |
|--------|---------------------|
| `updateValidity(validity, state)` | call `super` (it renders the error message + sets `data-cmp-valid`), then mirror `aria-invalid` onto extra sub-widgets |
| `updateReadOnly(readOnly, state)` | call `super` (sets `data-cmp-readonly`), then set `aria-readonly`/`readonly` on sub-widgets |
| `updateEnabled(enabled, state)` | call `super` (sets `data-cmp-enabled`), then toggle `disabled` on sub-widgets |
| `updateValidity(validity, state)` | call `super` first, then mirror `aria-invalid` onto extra sub-widgets |
| `updateReadOnly(readOnly, state)` | call `super` first, then set `aria-readonly`/`readonly` on sub-widgets |
| `updateEnabled(enabled, state)` | call `super` first, then toggle `disabled` on sub-widgets |
| `updateValue(value)` | end with `this.updateEmptyStatus()`; for composite widgets, don't repopulate a focused sub-input |
| `setModel(model)` | register focus/blur listeners for `setActive()`/`setInactive()` |

Expand Down
62 changes: 62 additions & 0 deletions .claude/skills/create-core-component/references/common-mistakes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Common Implementation Mistakes

1. **`data-cmp-is` mismatch** — The single most common failure. The string in HTML must equal `static IS` in JS. One extra capital letter breaks the wiring.

2. **Wrong `fieldType` format** — Core uses kebab-case: `text-input`, not `textInput` or `TEXT_INPUT`. Check `references/component-anatomy.md` (FieldType Values section) for the full list.

3. **`@Inject` instead of `@ValueMapValue`** — `@Inject` uses Sling's default injector chain and may pick up values from places other than the resource value map. Always use `@ValueMapValue` for JCR properties.

4. **Missing `InjectionStrategy.OPTIONAL`** — Without it, Sling Model adaptation fails at runtime if the JCR property is absent on an existing content node.

5. **Foundation package imports in Java** — Any import starting with `com.adobe.guides.*` or `com.adobe.fd.guide.*` means you are cross-referencing foundation. Remove it.

6. **Boolean `data-cmp-*` attributes as bare booleans** — `data-cmp-visible="${model.visible}"` outputs `true`/`false` as strings correctly in some HTL versions but fails in others. Always use `${model.visible ? 'true' : 'false'}`.

7. **Missing `aria-describedby`** — All interactive widgets must reference all three IDs (`{id}__errormessage`, `{id}__longdescription`, `{id}__shortdescription`). Omitting any of them breaks screen reader support and fails accessibility audits.

8. **Clientlib category typo** — The category in `runtime/.content.xml` must exactly match what you embed in the `core-forms-components-runtime-all` clientlib. A single character difference means the component's JS/CSS never loads.

9. **Step component extending FormFieldBase** — Steps have no widget and no value. Extending `FormFieldBase` instead of `FormContainer` causes null errors on `getWidget()`.

10. **`fd:viewType` on field components** — `fd:viewType` is for step/display components that share `fieldType=plain-text` but need distinct rendering. Field components (text-input, number-input, etc.) do not need `fd:viewType`.

11. **CSS properties in the core-forms-components stub file** — `{name}view.css` inside `aem-core-forms-components` must have empty rules only. Visual properties go in `aem-forms-theme-canvas`.

12. **Hardcoded colour or size values in theme SCSS** — Always use design tokens from `_variables.scss`. Hardcoded values (`#666`, `40px`) break when the theme is customised. If no suitable token exists, add one to `_variables.scss`.

13. **Forgetting `[data-cmp-valid]` selectors in theme SCSS** — Without these, validation state changes written by the JS view produce no visual feedback. Add both `[data-cmp-valid=false]` and `[data-cmp-valid=true]` selectors for every interactive field component.

14. **Not importing the new SCSS file in `theme.scss`** — The Parcel build only includes what is imported. A missing `@import` means the component has zero styles even after `npm run build`.

15. **`data-sly-test` on the `__value` div** — Never put `data-sly-test` on the outer `__value` wrapper. `getWidget()` in the JS view must always find `.cmp-adaptiveform-{name}__value`. Put the conditional **inside** the div with a `<sly data-sly-test="…">` wrapper.

16. **Destructive `updateValue` fallback** — Do not set `this.element.innerHTML` when `getWidget()` returns null. That wipes the `__label-container`. Update only `getWidget()`; fix HTL if the widget is missing.

17. **Invented dialog text during migration** *(migration mode only)* — Adding `fieldDescription` to Core dialog fields that had no description in the foundation dialog, or rephrasing foundation `fieldLabel` / option `text` without justification. Rule: copy foundation text verbatim; omit `fieldDescription` when the foundation field had none; only write original text for fields that are genuinely new in Core.

---

## Display/Text Component Mistakes

These apply to any component that uses HTL template 7c and `sling:resourceSuperType="core/fd/components/form/text/v1/text"`.

1. **Title not rendering on display/text components** — Symptom: only rich-text `value` shows but `jcr:title` is set in CRX. Causes and fixes:
- HTL used `resource.properties['jcr:title']` instead of `baseModel.label.value` → use HTL template 7c with `data-sly-use.baseModel="…Base"` (see "Sling Model – Display/Text" in `references/templates.md`).
- Sling Model `adapters` omitted `Base.class` → add `Base.class` alongside `Text.class`.
- Bound only `Text` in HTL for label → `Text` has no `getLabel()`; extend `AbstractBaseImpl` and register `Base.class` in `adapters`.
- Parent `text/v1/text` dialog hides Title → re-declare `./jcr:title` and `./hideTitle` in child `_cq_dialog`.
- **Verify:** CRX has `jcr:title`; DevTools shows `label.cmp-adaptiveform-{name}__label` inside `__label-container`.

---

## Author Dialog and Coral Interactivity

These apply to any component whose author dialog uses composite multifields or JavaScript-driven conditional field visibility. `references/editor-clientlib.md` is the canonical source for the patterns and fixes below (JS boilerplate, `getSelectValue`, `setVisible`) — this list only flags the symptoms so you recognize them.

1. **Coral listener ordering race** — a `coral-collection:add` handler never fires for a programmatically-added item. See "Listener registration order" in `references/editor-clientlib.md`.
2. **Both conditional fields visible on new multifield item** — adding an item briefly shows all conditionally-hidden fields before they settle. See "Default-off fields in new items" in `references/editor-clientlib.md`.
3. **`getSelectValue` returns nothing** — reading `.val()` on a `granite:class`-wrapped element instead of the `coral-select` DOM property. See `getSelectValue` in `references/editor-clientlib.md`.
4. **`setVisible` hides more than intended** — `.closest(".coral-Form-fieldwrapper")` traverses past the multifield item boundary to a parent container's fieldwrapper. See "`setVisible` boundary" in `references/editor-clientlib.md`.
5. **Select default not applied when `emptyText` omitted** — the JS may read `val === ""` before Coral finishes upgrading. See "Select Default Behavior" in `references/editor-clientlib.md`.

---
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,7 @@ FormComponent

## Implementation Hierarchy

```
AbstractComponentImpl (WCM Core)
└── AbstractFormComponentImpl (name, fieldType, dataRef, visible, value, events, rules)
└── AbstractBaseImpl (label, description, tooltip, type, required, enabled)
└── AbstractFieldImpl (readOnly, default, placeholder, min/max constraints)
├── AbstractOptionsFieldImpl (enum, enumNames, enforceEnum)
└── [Direct subclasses for simple fields]
```

Choose the appropriate base class:
- **Simple text/number field** -> extend `AbstractFieldImpl`
- **Options-based field** (checkboxes, radios, selects) -> extend `AbstractOptionsFieldImpl`
- **Container/panel** -> extend `AbstractContainerImpl`
- **Non-input component** (button, text, image) -> extend `AbstractBaseImpl`
- **Composite / split widget** (e.g. day/month/year, multiple visible inputs feeding
one value) -> still extend the field base that matches the data type
(`AbstractFieldImpl` for date), but render **one hidden combined `<input>`** as the
value-bearing widget plus the visible sub-inputs. This category has extra runtime
rules — read `references/runtime-view-js.md` before writing the view JS.
For the full class hierarchy (Impl classes) and base-class selection guide (including the composite / split-widget case), see `docs/architecture/overview.md` — the "Java Model Hierarchy" section is the single source of truth; do not restate it here.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Composite Multifield in Granite UI Dialogs

Use a `composite="true"` multifield when each item in a repeatable list has multiple named sub-fields (e.g., a list of signers each with email, authentication method, and phone number). Each item is stored as a child JCR node under the multifield's named path.

---

## 1. XML Structure

```xml
<myMultifield
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="true"
fieldDescription="Add one or more items."
fieldLabel="Items"
name="./items/item">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./items/item">
<items jcr:primaryType="nt:unstructured">
<!-- Sub-fields — note: NO "./" prefix on name attributes here -->
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="title"/>
<mode
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Mode"
name="mode">
<items jcr:primaryType="nt:unstructured">
<opt1 jcr:primaryType="nt:unstructured" text="Option A" value="A"/>
<opt2 jcr:primaryType="nt:unstructured" text="Option B" value="B"/>
</items>
</mode>
</items>
</field>
</myMultifield>
```

**Critical rules:**
- `composite="true"` (not `composite="{Boolean}true"`) — the literal string form is required.
- The `field` container's `name` must match the multifield's `name` exactly (including the `./` prefix).
- Sub-field `name` attributes inside the template do NOT use `./` prefix — they are relative to the multifield item node automatically.
- The `granite:class` on the `field` container provides the JS hook for the whole item row.

---

## 2. JCR Storage

Each item is stored as a numbered child of the multifield node:
```
./items/item/item0/title = "First item title"
./items/item/item0/mode = "A"
./items/item/item1/title = "Second item title"
./items/item/item1/mode = "B"
```

---

## 3. Reading in a Sling Model

```java
@ChildResource(injectionStrategy = InjectionStrategy.OPTIONAL, name = "items")
private Resource itemsResource;

@PostConstruct
private void init() {
if (itemsResource != null) {
Resource itemContainer = itemsResource.getChild("item");
if (itemContainer != null) {
List<MyItemModel> list = new ArrayList<>();
for (Resource child : itemContainer.getChildren()) {
MyItemModel m = child.adaptTo(MyItemModel.class);
if (m != null) list.add(m);
}
items = Collections.unmodifiableList(list);
}
}
}
```

The item model class must be annotated with `adaptables = { Resource.class }` (not `SlingHttpServletRequest.class`) since it adapts from child resources, not from a request.

---

## 4. JS Selector for Sub-Fields

Inside a `coral-collection:add` handler, `e.detail.item` is the `coral-multifield-item` element. The item div (from `granite:class` on the `field` container) is a child of it — find it with `$(e.detail.item).find(".your-item-class")`. The template HTML is fully rendered (including `granite:class` values) when the event fires, but Coral sub-components (`coral-select`, `coral-radiogroup`) may still be upgrading — use `Coral.commons.ready(e.detail.item, callback)` before reading `coral-select.value`.

**Also see:** `references/editor-clientlib.md` for the full JS boilerplate pattern including listener registration order, default-off field hiding, and `getSelectValue` helpers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# CSS Two-Layer Architecture

## How CSS is Split Between the Two Repos

```
aem-core-forms-components aem-forms-theme-canvas
───────────────────────────────────── ──────────────────────────────────────
ui.af.apps/.../form/{name}/v1/{name}/ src/components/{name}/_{name}.scss
clientlibs/
runtime/
css.txt ← lists {name}view.css src/site/_variables.scss (design tokens)
css/
{name}view.css ← EMPTY STUBS src/theme.scss ← imports all components
dist/theme.css ← compiled output
→ deployed as AEM clientlib
```

**Rule**: Never put visual CSS properties in `aem-core-forms-components`. Never put only BEM structure definitions in `aem-forms-theme-canvas`.

## What Goes in Each Layer

| | `aem-core-forms-components` stub CSS | `aem-forms-theme-canvas` SCSS |
|-|--------------------------------------|-------------------------------|
| BEM class declarations | All classes, empty `{ }` | Same class names, with properties |
| Colors, spacing, fonts | Never | Via `$variables` tokens |
| Validation states | Never | `[data-cmp-valid=false/true]` selectors |
| Focus / hover / disabled | Never | Pseudo-class selectors |
| Responsive breakpoints | Never | Media queries |
| Design tokens | Never | `_variables.scss` |
| Build step | None — plain CSS | Parcel 2.0 → `npm run build` |

## Standard BEM Elements for Every Field Component

This table is the single source of truth for the standard BEM element set; other references point here.

Every field component's HTL template emits these elements. The stub CSS and theme SCSS must cover all of them:

| BEM element suffix | CSS class | What it wraps |
|-------------------|-----------|---------------|
| *(root block)* | `.cmp-adaptiveform-{name}` | Outer container `<div>` |
| `__label-container` | `.cmp-adaptiveform-{name}__label-container` | Label + question mark row |
| `__label` | `.cmp-adaptiveform-{name}__label` | The `<label>` element |
| `__questionmark` | `.cmp-adaptiveform-{name}__questionmark` | Help icon |
| `__widget` | `.cmp-adaptiveform-{name}__widget` | The `<input>`, `<select>`, `<textarea>`, or custom `<div>` |
| `__shortdescription` | `.cmp-adaptiveform-{name}__shortdescription` | Tooltip / short help text |
| `__longdescription` | `.cmp-adaptiveform-{name}__longdescription` | Long description paragraph |
| `__errormessage` | `.cmp-adaptiveform-{name}__errormessage` | Validation error text |

Validation state attribute selectors (set by JS view at runtime):

```scss
.cmp-adaptiveform-{name}[data-cmp-valid=false] { ... } // invalid
.cmp-adaptiveform-{name}[data-cmp-valid=true] { ... } // valid
```

Visibility/state attributes (set by JS view at runtime):

```scss
.cmp-adaptiveform-{name}[data-cmp-visible=false] { display: none; }
.cmp-adaptiveform-{name}[data-cmp-enabled=false] { opacity: 0.5; pointer-events: none; }
```

## Build and Deploy the Theme

```bash
# in aem-forms-theme-canvas/
npm run build # Parcel compiles all SCSS → dist/theme.css
npm run create-clientlib # packages dist/ as AEM clientlib (outputs to theme-clientlibs/)
# then install theme-clientlibs/ package into AEM via Package Manager
```
Loading
Loading